Linux系统中的进程间通信方式主要以下几种:
同一主机上的进程通信方式
网络主机间的进程通信方式
各自的特点如下:
- 管道(PIPE):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系(父子进程)的进程间使用。另外管道传送的是无格式的字节流,并且管道缓冲区的大小是有限的(管道缓冲区存在于内存中,在管道创建时,为缓冲区分配一个页面大小)。
- 有名管道 (FIFO): 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
- 信号(Signal): 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
- 信号量(Semaphore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
- 消息队列(Message Queue):消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 共享内存(Shared Memory ):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
- 套接字(Socket): 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机间的进程通信。
线程间通信机制:
线程是一种轻量级的进程。 进程的通信机制主要包括无名管道、有名管道、消息队列、信号量、共享内存以及信号等。这些机制都是由linux内核来维护的,实现起来都比较复杂,而且占用大量的系统资源。 线程间的通信机制实现起来则相对简单,主要包括互斥锁、条件变量、读写锁和线程信号、信号量等。互斥锁通信机制:1、互斥锁基本原理:互斥锁以排他的方式防止数据被并发修改。当多个线程共享相同的内存时,需要确保每个线程看到的数据是一样的。如果是只读,那么一定是一样的。如果是可读可写,在一个线程操作一个内存区域时,包含三个步骤,即读出数据,修改数据,写回数据。如果该线程在没有写回数据前,另一个线程来访问同一个区域,如果是读,得不到最新的数据状态,如果是写,则会造成数据覆盖等问题。互斥锁就两个状态:开锁(0),上锁(1)。将某个共享资源和互斥锁绑定后,对该共享资源的访问操作如下:A】在访问资源前,首先申请该互斥锁,如果在开锁状态,则申请到该锁对象,并立即占有该锁(锁定)。以防其他线程访问修改此资源。如果该锁处于锁定状态,默认阻塞等待。B】原则上只有锁定该互斥锁的进程才能释放此互斥锁。但实际上,非锁定的线程去解锁也能成功。这个与锁的条件有关,本文后续内容会详细介绍。互斥锁基本操作函数如下:功能 函数初始化互斥锁 pthread_mutex_init()阻塞申请互斥锁 pthread_mutex_lock()释放互斥锁 pthread_mutex_unlock()尝试加锁(非阻塞方式)pthread_mutex_trylock()销毁互斥锁 pthread_mutex_destroy()2、互斥锁的初始化和销毁:pthread_mutex_init()、pthread_mutex_destroy()A】头文件:#include <pthread.h>B】函数原型:extern int pthread_mutex_init(pthread_mutex_t *__mutex,__const pthread_mutexattr_t *__mutexattr);extern int pthread_mutex_destroy(pthread_mutex_t *__mutex);C】返回:操作成功返回0,不成功则返回非零值D】参数:a、第一个参数为指向要初始化/销毁的互斥锁的指针。pthread_mutex_t即互斥量类型。在使用互斥锁时,需在函数内定义一个这种类型的变量。其值可通过pthread_mutex_init()函数来以初始化,也可以通过使用pthread.h中定义的宏PTHREAD_MUTEX_INITIALIZER (只对静态分配的互斥量)来初始化。如果是动态分配的互斥量,那么释放内存前需要用pthread_mutex_destroy,初始化用pthread_mutex_init()。pthread.h中宏定义如下:#define PTHREAD_MUTEX_INITIALIZER { {0,} }初始化方式如下:pthread_mutex_t p = PTHREAD_MUTEX_INITIALIZER;b、第二个参数mutexattr是指向属性对象的指针,该属性对象定义要初始化锁的属性。如果该指针为NULL,则表示使用默认属性。锁的属性在本文后续部分有详细的介绍。3、互斥锁的申请、释放和尝试解锁:pthread_mutex_lock()、pthread_mutex_unlock()、pthread_mutex_trylock()A】函数原型:extern int pthread_mutex_lock(pthread_mutex_t *__mutex);extern int pthread_mutex_trylock(pthread_mutex_t *__mutex);extern int pthread_mutex_unlock(pthread_mutex_t *__mutex);B】返回:成功返回0,失败返回一个错误编号,以指明错误。(pthread_mutex_unlock()未设置errno变量)
条件变量通信机制:1、条件变量基本原理:条件变量的出现,可以弥补互斥锁的缺陷,有些问题仅仅靠互斥锁无法解决。但是条件变量不能单独使用,必须配合互斥锁一起实现对资源的互斥访问。例:互斥锁无法解决的问题。int i = 3;int j = 7;pthread Apthread_Bpthread_mutex_lock();pthread_mutex_lock(){ {i++; if(i==j)j--; do_something();} }pthread_mutex_unlock();pthread_mutex_unlock(); ——————————————————————————————————————上例中:两个线程抢占互斥锁,可能会导致pthread B中的do_something()函数永远无法执行的情况。这是程序员不想看到的。仔细分析后,可得到线程B其实不需要一直的申请释放锁,其运行仅仅需要一种情况而已。在A线程满足i == j时,通知B线程执行do_something()即可。条件变量基本操作:功能函数初始化条件变量pthread_cond_init()阻塞等待条件变量pthread_cond_wait()通知等待该条件变量的第一个线程pthread_cond_signal()在指定的时间之内等待条件变量pthread_cond_timedwait()销毁条件变量状态pthread_cond_destroy()2、条件变量的初始化和销毁:pthread_cond_init()、pthread_cond_destroy()A】函数原型:extern int pthread_cond_init(pthread_cond_t *__restrict __cond,__const pthread_condattr_t *__restrict __cond_attr);extern int pthread_cond_destroy(pthread_cond_t *__cond);B】返回:成功返回0,失败返回错误编号以指明错误。C】参数:第一个参数指向要初始化或损坏的条件变量的指针,条件变量的类型为pthread_cond_t。第二个参数指向条件属性对象的指针。该属性对象定义要初始化的条件变量的特性,如果此变量初始化为NULL,则为默认属性。关于条件属性,本文后续会有详细介绍。3、通知等待条件变量的线程:pthread_cond_signal()、pthread_cond_broadcast()A】函数原型:extern int pthread_cond_signal(pthread_cond_t *__cond);extern int pthread_cond_broadcast(pthread_cond_t *__cond);B】说明:a、pthread_cond_signal()函数用于唤醒等待出现与条件变量cond相关联的条件的第一个线程。如果cond上没有阻塞任何线程,则此函数不起作用。如果cond阻塞了多个线程,则调度策略将确定要取消阻塞的线程。显然,在此函数中,隐含的释放了当前线程占用的信号量(备注:信号和信号量不是一个东西,在进程和进程通信中会详细说明信号和信号量)。b、pthread_cond_broadcast()函数用于唤醒等待出现与条件变量cond关联的条件的所有线程。如果cond上没有阻塞任何线程,则此函数不起作用。4、等待条件变量:pthread_cond_wait()、pthread_cond_timedwait()A】函数原型:extern int pthread_cond_wait(pthread_cond_t *__restrict __cond,pthread_mutex_t *__restrict __mutex);extern int pthread_cond_timedwait(pthread_cond_t *__restrict __cond,pthread_mutex_t *__restrict __mutex ,__const struct timespec *__restrict __abstime);B】参数说明:cond是指向要等待的条件变量的指针,mutex指向与条件变量cond关联的互斥锁的指针。pthread_cond_wait()、pthread_cond_timedwait()函数的实现是一个先对互斥锁进行解锁,再加锁的一个过程。pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)函数传入的参数mutex用于保护条件,因为我们在调用pthread_cond_wait时,如果条件不成立我们就进入阻塞,但是进入阻塞这个期间,如果条件变量改变了的话,那我们就漏掉了这个条件。因为这个线程还没有放到等待队列上,所以调用pthread_cond_wait前要先锁互斥量,即调用pthread_mutex_lock(),pthread_cond_wait在把线程放进阻塞队列后,自动对mutex进行解锁,使得其它线程可以获得加锁的权利。这样其它线程才能对临界资源进行访问并在适当的时候唤醒这个阻塞的进程。当pthread_cond_wait返回的时候又自动给mutex加锁。Thread A:当满足条件的时候发送一个信号。Thread B:先给一个mutex加锁,以便互斥访问count的值。在一个while循环里等待count值达到MAX_COUNT。因为当某个条件满足时,可能会有多个线程被唤醒。所以需要判断条件是否还满足。pthread_cond_wait首先把调用线程放入条件变量的等待队列,然后再释放mutex。当函数返回时,mutex又会被加上锁。最后对mutex解锁,让其他线程使用count变量。(加了写锁的等待就是占着茅坑不拉屎,有数据更新此操作域又执行不了写操作,只能先解锁咯~~~~)
pthread_cond_timedwait()多了一个参数,即abstime,abstime是从1970年1月1日00:00:00以来的秒数,是一个绝对时间。等待时间到,则不阻塞,往下执行程序。timespec结构体声明如下:struct timespec{long tv_sec;long tv_nsec;};C】返回:如果成功,返回0,失败则返回一个错误编号。
实例:
#include<stdlib.h>
#include<stdio.h>
#include<pthread.h>
#include<errno.h>
#include<unistd.h>
typedef void* (*fun)(void*);
int x=1,y=4;
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void* thread1(void*);
void* thread2(void*);
int main(int argc, char** argv)
{
printf("enter main!\n");
pthread_t tid1, tid2;
int rc1=0, rc2=0;
rc2 = pthread_create(&tid2, NULL, thread2, NULL);
if(rc2 != 0)
printf("%s: %d\n",__func__, strerror(rc2));
rc1 = pthread_create(&tid1, NULL, thread1, &tid2);
if(rc1 != 0)
printf("%s: %d\n",__func__, strerror(rc1));
sleep(1);
printf("leave main!\n");
exit(0);
}
void* thread1(void* arg)
{
printf("enter thread1\n");
printf("this is thread1: x= %d,y=%d. thread id is %u\n",x,y, (unsigned int)pthread_self());
pthread_mutex_lock(&mutex);
x+=y;
if(x>y)
pthread_cond_signal(&cond);
printf("this is thread1: x= %d,y=%d. thread id is %u\n", x,y, (unsigned int)pthread_self());
pthread_mutex_unlock(&mutex);
pthread_join(*(pthread_t*)arg, NULL);
printf("leave thread1\n");
pthread_exit(0);
}
void* thread2(void* arg)
{
printf("enter thread2\n");
printf("this is thread2: x= %d,y=%d. thread id is %u\n", x,y, (unsigned int)pthread_self());
pthread_mutex_lock(&mutex);
while(x<=y)
pthread_cond_wait(&cond, &mutex);
x-=y;
printf("this is thread2: x= %d,y=%d. thread id is %u\n", x,y, (unsigned int)pthread_self());
pthread_mutex_unlock(&mutex);
printf("leave thread2\n");
pthread_exit(0);
}
有的时候仅仅依靠锁住共享资源来使用它是不够的。有时候共享资源只有某些状态的时候才能够使用。比方说,某个线程如果要从堆栈中读取数据,那么如果栈中没有数据就必须等待数据被压栈。这种情况下的同步使用互斥锁
是不够的。另一种同步的方式--条件变量,就可以使用在这种情况下。条件变量(Condition Variable)是线程间的一种同步机制,提供给两个线程协同完成任务的一种方法,使用条件变量可以以原子方式阻塞线程,直到某个特定条件为真为止。条件变量的测试一般是用互斥量来保护的,用来确保每一时刻只有一个线程能够改变条件变量,如果条件为假,线程通常会基于条件变量而阻塞,并以原子方式释放等待条件变化的互斥锁。如果另一个线程更改了条件,该线程可能会向相关的条件变量发出信号,从而使一个或多个等待的线程执行以下操作:
- 唤醒
- 再次获取互斥锁
- 重新评估条件
在以下情况下,条件变量可用于在进程之间同步线程:
- 线程是在可以写入的内存中分配的
- 内存由协作进程共享
Condition Variable用pthread_cond_t类型的变量表示,和Mutex的初始化和销毁类似,pthread_cond_init函数初始化一个Condition Variable,attr参数为NULL则表示缺省属性,pthread_cond_destroy函数销毁一个Condition Variable。如果ConditionVariable是静态分配的,也可以用宏定义PTHEAD_COND_INITIALIZER初始化,相当于用pthread_cond_init函数初始化并且attr参数为NULL。
条件变量的相关函数如下:
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cptr, pthread_mutex_t *mptr);
int pthread_cond_signal(pthread_cond_t *cptr);
//Both return: 0 if OK, positive Exxx value on error
pthread_cond_wait 用于等待某个特定的条件为真,一个线程可以调用pthread_cond_wait在一个Condition Variable上阻塞等待,这个函数做以下三步操作:
1. 释放Mutex
2. 阻塞等待
3. 当被唤醒时,重新获得Mutex并返回
注意:3个操作是原子性的操作,之所以一开始要释放Mutex,是因为需要让其他线程进入临界区去更改条件,或者也有其他线程需要进入临界区等待条件。
pthread_cond_signal 用于通知阻塞的线程某个特定的条件为真了。在调用者两个函数之前需要声明一个pthread_cond_t 类型的变量,用于这两个函数的参数。
为什么条件变量始终与互斥锁一起使用,对条件的测试是在互斥锁(互斥)的保护下进行的呢?因为“某个特性条件”通常是在多个线程之间共享的某个变量。互斥锁允许这个变量可以在不同的线程中设置和检测。
通常, pthread_cond_wait 只是唤醒等待某个条件变量的一个线程。如果需要唤醒所有等待某个条件变量的线程,需要调用:
int pthread_cond_broadcast (pthread_cond_t * cptr);
默认情况下面,阻塞的线程会一直等待,知道某个条件变量为真。如果想设置最大的阻塞时间可以调用:
int pthread_cond_timedwait (pthread_cond_t * cptr, pthread_mutex_t *mptr, const struct timespec *abstime);
如果时间到了,条件变量还没有为真,仍然返回,返回值为ETIME。
二、条件变量使用规范
(一)、等待条件代码
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
(二)、给条件发送通知代码
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);
至于为什么在被唤醒之后还要再次进行条件判断(即为什么要使用while循环来判断条件),是因为可能有“惊群效应”。有人觉得此处既然是被唤醒的,肯定是满足条件了,其实不然。如果是多个线程都在等待这个条件,而同时只能有一个线程进行处理,此时就必须要再次条件判断,以使只有一个线程进入临界区处理。考虑以下情况:
1,pthread_cond_signal在多处理器上可能同时唤醒多个线程,当你只能让一个线程处理某个任务时,其它被唤醒的线程就需要继续 wait,while循环的意义就体现在这里了,而且规范要求pthread_cond_signal至少唤醒一个pthread_cond_wait上的线程,其实有些实现为了简单在单处理器上也会唤醒多个线程.
2,某些应用,如线程池,pthread_cond_broadcast唤醒全部线程,但我们通常只需要一部分线程去做执行任务,所以其它的线程需要继续wait.所以强烈推荐此处使用while循环.
其实说白了很简单,就是pthread_cond_signal()也可能唤醒多个线程,而如果你同时只允许一个线程访问的话,就必须要使用while来进行条件判断,以保证临界区内只有一个线程在处理。