作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明。谢谢!
典型的UNIX系统都支持一个进程创建多个线程(thread)。在Linux进程基础中提到,Linux以进程为单位组织操作,Linux中的线程也都基于进程。尽管实现方式有异于其它的UNIX系统,但Linux的多线程在逻辑和使用上与真正的多线程并没有差别。
一. 多线程
我们先来看一下什么是多线程。在Linux从程序到进程中,我们看到了一个程序在内存中的表示。这个程序的整个运行过程中,只有一个控制权的存在。当函数被调用的时候,该函数获得控制权,成为激活(active)函数,然后运行该函数中的指令。与此同时,其它的函数处于离场状态,并不运行。如下图所示:
我们看到,各个方块之间由箭头连接。各个函数就像是连在一根线上一样,计算机像一条流水线一样执行各个函数中定义的操作。这样的一个程序叫做单线程程序。
多线程就是允许一个进程内存在多个控制权,以便让多个函数同时处于激活状态,从而让多个函数的操作同时运行。即使是单CPU的计算机,也可以通过不停地在不同线程的指令间切换,从而造成多线程同时运行的效果。如下图所示,就是一个多线程的流程:
main()到func3()再到main()构成一个线程,此外func1()和func2()构成另外两个线程。操作系统一般都有一些系统调用来让你将一个函数运行成为一个新的线程。
回忆我们在Linux从程序到进程中提到的stack的功能和用途。由于stack中只有最下方的stack frame可以被读取,所以我们只能有该frame对应的单一函数处于激活状态。为了实现多线程,我们必须绕开stack带给我们的限制。为此,当我们创建一个新的线程的时候,我们为这个线程创建一个新的stack。当该stack执行到全部弹出的时候,该线程完成任务并结束。所以,多线程的进程在内存中会有多个stack,相互之间以一定的空白区域隔开,以备stack的增长。每个线程随时可以使用自己stack中最下方的stack frame中的参数和变量,并与其它线程共享内存中的Text,heap和global data区域。在上面的例子中,我们将在进程空间中有三个stack。
(要注意的是,对于多线程来说,由于同一个进程空间中存在多个stack,任何一个空白区域被填满都会导致stack overflow的问题。)
2. 并发
多线程相当于一个并发(concunrrency)系统。并发系统一般同时执行多个任务。如果多个任务可以共享资源,特别是同时写入某个变量的时候,就需要解决同步的问题。比如说,我们有一个多线程火车售票系统,用全局变量i存储剩余的票数。多个线程不断地卖票(i = i - 1),直到剩余票数为0。所以每个都需要执行如下操作:
/*mu is a global mutex*/
while (1) { /*infinite loop*/
if (i != 0) i = i -1
else { printf("no more tickets"); exit(); } }
如果只有一个线程执行上面的程序的时候(相当于一个窗口售票),则没有问题。但如果多个线程都执行上面的程序(相当于多个窗口售票), 我们就会出现问题。我们会看到,其根本原因在于同时发生的各个线程都可以对i读取和写入。
我们这里的if结构会给CPU两个指令, 一个是判断是否有剩余的票(i != 0), 一个是卖票 (i = i -1)。某个线程会先判断是否有票(比如说此时i为1),但两个指令之间存在一个时间窗口,其它线程可能在此时间窗口内执行卖票操作(i = i -1),导致该线程卖票的条件不再成立。但该线程由于已经执行过了判断指令,所以无从知道i发生了变化,所以继续执行卖票指令,以至于卖出不存在的票 (i成为负数)。对于一个真实的售票系统来说,这将成为一个严重的错误 (售出了过多的票,火车爆满)。
在并发情况下,指令执行的先后顺序由内核决定。同一个线程内部,指令按照先后顺序执行,但不同线程之间的指令很难说清除哪一个会先执行。如果运行的结果依赖于不同线程执行的先后的话,那么就会造成竞争条件(race condition),在这样的状况下,计算机的结果很难预知。我们应该尽量避免竞争条件的形成。最常见的解决竞争条件的方法是将原先分离的两个指令构成不可分隔的一个原子操作(atomic operation),而其它任务不能插入到原子操作中。
3. 多线程同步(synchronization)
对于多线程程序来说,同步是指在一定的时间内只允许某一个线程访问某个资源 。而在此时间内,不允许其它的线程访问该资源。我们可以通过互斥锁(mutex),条件变量(condition variable)和读写锁(reader-writer lock)来同步资源。
1) mutex
尽管在Posix Thread中同样可以使用IPC的信号量机制来实现互斥锁mutex功能,但显然semphore的功能过于强大了,在Posix Thread中定义了另外一套专门用于线程同步的mutex函数。
1. 创建和销毁
有两种方法创建互斥锁,静态方式和动态方式。
1).POSIX定义了一个宏PTHREAD_MUTEX_INITIALIZER来静态初始化互斥锁,方法如下:
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
在LinuxThreads实现中,pthread_mutex_t是一个结构,而PTHREAD_MUTEX_INITIALIZER则是一个结构常量。
2).动态方式是采用pthread_mutex_init()函数来初始化互斥锁,API定义如下:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
其中mutexattr用于指定互斥锁属性(见下),如果为NULL则使用缺省属性。
pthread_mutex_destroy()用于注销一个互斥锁,API定义如下:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态。由于在Linux中,互斥锁并不占用任何资源,因此LinuxThreads中的pthread_mutex_destroy()除了检查锁状态以外(锁定状态则返回EBUSY)没有其他动作。
2. 互斥锁属性
互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。当前(glibc2.2.3,linuxthreads0.9)有四个值可供选择:
PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。
3. 锁操作
锁操作主要包括加锁pthread_mutex_lock()、解锁pthread_mutex_unlock()和测试加锁pthread_mutex_trylock()三个,不论哪种类型的锁,都不可能被两个不同的线程同时得到,而必须等待解锁。对于普通锁和适应锁类型,解锁者可以是同进程内任何线程;而检错锁则必须由加锁者解锁才有效,否则返回EPERM;对于嵌套锁,文档和实现要求必须由加锁者解锁,但实验结果表明并没有这种限制,这个不同目前还没有得到解释。在同一进程中的线程,如果加锁后没有解锁,则任何其他线程都无法再获得锁。
1 int pthread_mutex_lock(pthread_mutex_t *mutex) 2 int pthread_mutex_unlock(pthread_mutex_t *mutex) 3 int pthread_mutex_trylock(pthread_mutex_t *mutex)
pthread_mutex_trylock()语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回EBUSY而不是挂起等待。
4. 其他
POSIX线程锁机制的Linux实现都不是取消点,因此,延迟取消类型的线程不会因收到取消信号而离开加锁等待。值得注意的是,如果线程在加锁后解锁前被取消,锁将永远保持锁定状态,因此如果在关键区段内有取消点存在,或者设置了异步取消类型,则必须在退出回调函数中解锁。
这个锁机制同时也不是异步信号安全的,也就是说,不应该在信号处理过程中使用互斥锁,否则容易造成死锁。
-----
mutex是一个特殊的变量,它有锁上(lock)和打开(unlock)两个状态。mutex一般被设置成全局变量。打开的mutex可以由某个线程获得。一旦获得,这个mutex会锁上,此后只有该线程有权打开。其它想要获得mutex的线程,会等待直到mutex再次打开的时候。我们可以将mutex想像成为一个只能容纳一个人的洗手间,当某个人进入洗手间的时候,可以从里面将洗手间锁上。其它人只能在mutex外面等待那个人出来,才能进去。在外面等候的人并没有排队,谁先看到洗手间空了,就可以首先冲进去。
上面的问题很容易使用mutex的问题解决,每个线程的程序可以改为:
/*mu is a global mutex*/
while (1) { /*infinite loop*/ mutex_lock(mu); /*aquire mutex and lock it, if cannot, wait until mutex is unblocked*/ if (i != 0) i = i - 1; else { printf("no more tickets"); exit(); } mutex_unlock(mu); /*release mutex, make it unblocked*/ }
第一个执行mutex_lock()的线程会先获得mu。其它想要获得mu的线程必须等待,直到第一个线程执行到mutex_unlock()释放mu,才可以获得mu,并继续执行线程。所以线程在mutex_lock()和mutex_unlock()之间的操作时,不会被其它线程影响,就构成了一个原子操作。
需要注意的时候,如果存在某个线程依然使用原先的程序 (即不尝试获得mu,而直接修改i),mutex不能阻止该程序修改i,mutex就失去了保护资源的意义。所以,mutex机制需要程序员自己来写出完善的程序来实现mutex的功能。我们下面讲的其它机制也是如此。
----
互斥锁用来保证一段时间内只有一个线程在执行一段代码。必要性显而易见:假设各个线程向同一个文件顺序写入数据,最后得到的结果一定是灾难性的。
先看下面一段代码。这是一个读/写程序,它们公用一个缓冲区,并且假定一个缓冲区只能保存一条信息。即缓冲区只有两个状态:有信息或没有信息。
1 void reader_function ( void ); 2 void writer_function ( void ); 3 4 char buffer; 5 int buffer_has_item=0; 6 pthread_mutex_t mutex; 7 struct timespec delay; 8 9 void main ( void ){ 10 pthread_t reader; 11 /* 定义延迟时间*/ 12 delay.tv_sec = 2; 13 delay.tv_nec = 0; 14 /* 用默认属性初始化一个互斥锁对象*/ 15 pthread_mutex_init (&mutex,NULL); 16 pthread_create(&reader, pthread_attr_default, (void *)&reader_function), NULL); 17 writer_function( ); 18 } 19 20 void writer_function (void){ 21 while(1){ 22 /* 锁定互斥锁*/ 23 pthread_mutex_lock (&mutex); 24 if (buffer_has_item==0){ 25 buffer=make_new_item( ); 26 buffer_has_item=1; 27 } 28 /* 打开互斥锁*/ 29 pthread_mutex_unlock(&mutex); 30 pthread_delay_np(&delay); 31 } 32 } 33 34 void reader_function(void){ 35 while(1){ 36 pthread_mutex_lock(&mutex); 37 if(buffer_has_item==1){ 38 consume_item(buffer); 39 buffer_has_item=0; 40 } 41 pthread_mutex_unlock(&mutex); 42 pthread_delay_np(&delay); 43 } 44 }
这里声明了互斥锁变量mutex,结构pthread_mutex_t为不公开的数据类型,其中包含一个系统分配的属性对象。函数pthread_mutex_init用来生成一个互斥锁。NULL参数表明使用默认属性。如果需要声明特定属性的互斥锁,须调用函数pthread_mutexattr_init。函数pthread_mutexattr_setpshared和函数pthread_mutexattr_settype用来设置互斥锁属性。前一个函数设置属性pshared,它有两个取值,PTHREAD_PROCESS_PRIVATE和PTHREAD_PROCESS_SHARED。前者用来不同进程中的线程同步,后者用于同步本进程的不同线程。在上面的例子中,使用的是默认属性PTHREAD_PROCESS_ PRIVATE。后者用来设置互斥锁类型,可选的类型有PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECURSIVE和PTHREAD _MUTEX_DEFAULT。它们分别定义了不同的上锁、解锁机制,一般情况下,选用最后一个默认属性。
pthread_mutex_lock声明开始用互斥锁上锁,此后的代码直至调用pthread_mutex_unlock为止,均被上锁,即同一时间只能被一个线程调用执行。当一个线程执行到pthread_mutex_lock处时,如果该锁此时被另一个线程使用,那此线程被阻塞,即程序将等待到另一个线程释放此互斥锁。在上面的例子中,使用了pthread_delay_np函数,让线程睡眠一段时间,就是为了防止一个线程始终占据此函数。
在使用互斥锁的过程中很有可能会出现死锁:两个线程试图同时占用两个资源,并按不同的次序锁定相应的互斥锁,例如两个线程都需要锁定互斥锁1和互斥锁2,a线程先锁定互斥锁1,b线程先锁定互斥锁2,这时就出现了死锁。此时可以使用函数pthread_mutex_trylock,它是函数pthread_mutex_lock的非阻塞版本,当它发现死锁不可避免时,它会返回相应的信息,程序员可以针对死锁做出相应的处理。另外不同的互斥锁类型对死锁的处理不一样,但最主要的还是要程序员自己在程序设计注意这一点。
2) condition variable
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
1. 创建和注销
条件变量和互斥锁一样,都有静态动态两种创建方式,静态方式使用PTHREAD_COND_INITIALIZER常量,如下:
pthread_cond_t cond=PTHREAD_COND_INITIALIZER
动态方式调用pthread_cond_init()函数,API定义如下:
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr)
尽管POSIX标准中为条件变量定义了属性,但在LinuxThreads中没有实现,因此cond_attr值通常为NULL,且被忽略。
注销一个条件变量需要调用pthread_cond_destroy(),只有在没有线程在该条件变量上等待的时候才能注销这个条件变量,否则返回EBUSY。因为Linux实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程。API定义如下:
int pthread_cond_destroy(pthread_cond_t *cond)
2. 等待和激发
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime)
等待条件有两种方式:无条件等待pthread_cond_wait()和计时等待pthread_cond_timedwait(),其中计时等待方式如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待,其中abstime以与time()系统调用相同意义的绝对时间形式出现,0表示格林尼治时间1970年1月1日0时0分0秒。
无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()(或pthread_cond_timedwait(),下同)的竞争条件(Race Condition)。mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP),且在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。
激发条件有两种形式,pthread_cond_signal()激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个;而pthread_cond_broadcast()则激活所有等待线程。
3. 其他
pthread_cond_wait()和pthread_cond_timedwait()都被实现为取消点,因此,在该处等待的线程将立即重新运行,在重新锁定mutex后离开pthread_cond_wait(),然后执行取消动作。也就是说如果pthread_cond_wait()被取消,mutex是保持锁定状态的,因而需要定义退出回调函数来为其解锁。
以下示例集中演示了互斥锁和条件变量的结合使用,以及取消对于条件等待动作的影响。在例子中,有两个线程被启动,并等待同一个条件变量,如果不使用退出回调函数(见范例中的注释部分),则tid2将在pthread_mutex_lock()处永久等待。如果使用回调函数,则tid2的条件等待及主线程的条件激发都能正常工作。
1 #include <stdio.h> 2 #include <pthread.h> 3 #include <unistd.h> 4 5 pthread_mutex_t mutex; 6 pthread_cond_t cond; 7 8 void * child1(void *arg) 9 { 10 pthread_cleanup_push(pthread_mutex_unlock,&mutex); /* comment 1 */ 11 while(1){ 12 printf("thread 1 get running \n"); 13 printf("thread 1 pthread_mutex_lock returns %d\n", 14 pthread_mutex_lock(&mutex)); 15 pthread_cond_wait(&cond,&mutex); 16 printf("thread 1 condition applied\n"); 17 pthread_mutex_unlock(&mutex); 18 sleep(5); 19 } 20 pthread_cleanup_pop(0); /* comment 2 */ 21 } 22 void *child2(void *arg) 23 { 24 while(1){ 25 sleep(3); /* comment 3 */ 26 printf("thread 2 get running.\n"); 27 printf("thread 2 pthread_mutex_lock returns %d\n", 28 pthread_mutex_lock(&mutex)); 29 pthread_cond_wait(&cond,&mutex); 30 printf("thread 2 condition applied\n"); 31 pthread_mutex_unlock(&mutex); 32 sleep(1); 33 } 34 } 35 int main(void) 36 { 37 int tid1,tid2; 38 printf("hello, condition variable test\n"); 39 pthread_mutex_init(&mutex,NULL); 40 pthread_cond_init(&cond,NULL); 41 pthread_create(&tid1,NULL,child1,NULL); 42 pthread_create(&tid2,NULL,child2,NULL); 43 do{ 44 sleep(2); /* comment 4 */ 45 pthread_cancel(tid1); /* comment 5 */ 46 sleep(2); /* comment 6 */ 47 pthread_cond_signal(&cond); 48 }while(1); 49 sleep(100); 50 pthread_exit(0); 51 }
如果不做注释5的pthread_cancel()动作,即使没有那些sleep()延时操作,child1和child2都能正常工作。注释3和注释4的延迟使得child1有时间完成取消动作,从而使child2能在child1退出之后进入请求锁操作。如果没有注释1和注释2的回调函数定义,系统将挂起在child2请求锁的地方;而如果同时也不做注释3和注释4的延时,child2能在child1完成取消动作以前得到控制,从而顺利执行申请锁的操作,但却可能挂起在pthread_cond_wait()中,因为其中也有申请mutex的操作。child1函数给出的是标准的条件变量的使用方式:回调函数保护,等待条件前锁定,pthread_cond_wait()返回后解锁。
条件变量机制不是异步信号安全的,也就是说,在信号处理函数中调用pthread_cond_signal()或者pthread_cond_broadcast()很可能引起死锁。
-------
condition variable是另一种常用的变量。它也常常被保存为全局变量,并和mutex合作。
假设我们有这样一个状况: 我们有100个工人,每人负责装修一个房间。当有10个房间装修完成的时候,我们就通知相应的十个工人一起去喝啤酒。我们如何实现呢?我们可以让工人在装修好房间之后,去检查已经装修好的房间数。但多线程条件下,会有竞争条件的危险(其他工人会在该工人装修好房子和检查之间完成工作)。
/*mu: global mutex, cond: global codition variable, num: global int*/
mutex_lock(mu) num = num + 1; /*worker build the room*/
if (num <= 10) { /*worker is within the first 10 to finish*/ cond_wait(mu, cond); /*wait*/ printf("drink beer"); } else if (num = 11) { /*workder is the 11th to finish*/ cond_broadcast(mu, cond); /*inform the other 9 to wake up*/ } mutex_unlock(mu);
通常, condition variable除了要和mutex配合之外,还需要和另一个全局变量配合(这里的num, 也就是装修好的房间数)。这个全局变量用来构成各个条件。
我们让工人在装修好房间(num = num + 1)之后,去检查已经装修好的房间数( num < 10 )。由于mu被锁上,所以不会有其他工人在此期间装修房间(改变num的值)。如果该工人是前十个完成的人,那么我们就调用cond_wait()函数。
cond_wait()做两件事情,一个是释放mu,从而让别的工人可以建房。另一个是等待,直到cond的通知。这样的话,符合条件的线程就开始等待。当有通知(第十个房间已经修建好)到达的时候,condwait()会再次锁上mu,并恢复线程的运行,我们会执行下一句prinft("drink beer") (我们以此来代表喝啤酒)。此后直到mutex_unlock()就构成了另一个mutex结构。
那么如何让前面十个调用cond_wait()的线程得到通知呢?我们注意到if还有另一种可能,也就是修建好第11个房间的人负责调用cond_broadcast()。它会给所有调用cond_wait()的线程放送通知,以便让那些线程恢复运行。
Condition variable特别适用于多个线程等待某个条件的发生。如果不使用Condition variable,那么每个线程就需要不断尝试获得mutex并检查条件是否发生,这样大大浪费了系统的资源。
1. 相关函数
1 #include <pthread.h> 2 pthread_cond_t cond = PTHREAD_COND_INITIALIZER; 3 int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t 4 *cond_attr); 5 int pthread_cond_signal(pthread_cond_t *cond); 6 int pthread_cond_broadcast(pthread_cond_t *cond); 7 int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); 8 int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t 9 *mutex, const struct timespec *abstime); 10 int pthread_cond_destroy(pthread_cond_t *cond);
2. 说明
条件变量是一种同步机制,允许线程挂起,直到共享数据上的某些条件得到满足。条件变量上的基本操作有:触发条件(当条件变为 true 时);等待条件,挂起线程直到其他线程触发条件。
条件变量要和互斥量相联结,以避免出现条件竞争--一个线程预备等待一个条件变量,当它在真正进入等待之前,另一个线程恰好触发了该条件。
pthread_cond_init 使用 cond_attr 指定的属性初始化条件变量 cond,当 cond_attr 为 NULL 时,使用缺省的属性。LinuxThreads 实现条件变量不支持属性,因此 cond_attr 参数实际被忽略。
pthread_cond_t 类型的变量也可以用 PTHREAD_COND_INITIALIZER 常量进行静态初始化。
pthread_cond_signal 使在条件变量上等待的线程中的一个线程重新开始。如果没有等待的线程,则什么也不做。如果有多个线程在等待该条件,只有一个能重启动,但不能指定哪一个。
pthread_cond_broadcast 重启动等待该条件变量的所有线程。如果没有等待的线程,则什么也不做。
pthread_cond_wait 自动解锁互斥量(如同执行了 pthread_unlock_mutex),并等待条件变量触发。这时线程挂起,不占用 CPU 时间,直到条件变量被触发。在调用 pthread_cond_wait 之前,应用程序必须加锁互斥量。pthread_cond_wait 函数返回前,自动重新对互斥量加锁(如同执行了 pthread_lock_mutex)。
互斥量的解锁和在条件变量上挂起都是自动进行的。因此,在条件变量被触发前,如果所有的线程都要对互斥量加锁,这种机制可保证在线程加锁互斥量和进入等待条件变量期间,条件变量不被触发。
pthread_cond_timedwait 和 pthread_cond_wait 一样,自动解锁互斥量及等待条件变量,但它还限定了等待时间。如果在 abstime 指定的时间内 cond 未触发,互斥量 mutex 被重新加锁,且 pthread_cond_timedwait 返回错误 ETIMEDOUT。abstime 参数指定一个绝对时间,时间原点与 time 和 gettimeofday 相同:abstime = 0 表示 1970 年 1 月 1 日 00:00:00 GMT。
pthread_cond_destroy 销毁一个条件变量,释放它拥有的资源。进入 pthread_cond_destroy 之前,必须没有在该条件变量上等待的线程。在 LinuxThreads 的实现中,条件变量不联结资源,除检查有没有等待的线程外,pthread_cond_destroy 实际上什么也不做。
3. 取消
pthread_cond_wait 和 pthread_cond_timedwait 是取消点。如果一个线程在这些函数上挂起时被取消,线程立即继续执行,然后再次对 pthread_cond_wait 和 pthread_cond_timedwait 在 mutex 参数加锁,最后执行取消。因此,当调用清除处理程序时,可确保,mutex 是加锁的。
4. 异步信号安全(Async-signal Safety)
条件变量函数不是异步信号安全的,不应当在信号处理程序中进行调用。特别要注意,如果在信号处理程序中调用 pthread_cond_signal 或 pthread_cond_boardcast 函数,可能导致调用线程死锁。
5. 返回值
在执行成功时,所有条件变量函数都返回 0,错误时返回非零的错误代码。
6. 错误代码
pthread_cond_init, pthread_cond_signal, pthread_cond_broadcast, 和 pthread_cond_wait 从不返回错误代码。
pthread_cond_timedwait 函数出错时返回下列错误代码:
ETIMEDOUT abstime 指定的时间超时时,条件变量未触发
EINTR pthread_cond_timedwait 被触发中断
pthread_cond_destroy 函数出错时返回下列错误代码:
EBUSY 某些线程正在等待该条件变量
7. 举例
1 2 设有两个共享的变量 x 和 y,通过互斥量 mut 保护,当 x > y 时,条件变量 cond 被触发。 3 int x,y; 4 int x,y; 5 pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER; 6 pthread_cond_t cond = PTHREAD_COND_INITIALIZER; 7 8 等待直到 x > y 的执行流程: 9 pthread_mutex_lock(&mut); 10 while (x <= y) { 11 pthread_cond_wait(&cond, &mut); 12 } 13 /* 对 x、y 进行操作 */ 14 pthread_mutex_unlock(&mut); 15 16 对 x 和 y 的修改可能导致 x > y,应当触发条件变量: 17 pthread_mutex_lock(&mut); 18 /* 修改 x、y */ 19 if (x > y) pthread_cond_broadcast(&cond); 20 pthread_mutex_unlock(&mut);
如果能够确定最多只有一个等待线程需要被唤醒(例如,如果只有两个线程通过 x、y 通信),则使用 pthread_cond_signal 比 pthread_cond_broadcast 效率稍高一些。如果不能确定,应当用 pthread_cond_broadcast。
要等待在 5 秒内 x > y,这样处理:
1 2 struct timeval now; 3 struct timespec timeout; 4 int retcode; 5 6 pthread_mutex_lock(&mut); 7 gettimeofday(&now); 8 timeout.tv_sec = now.tv_sec + 5; 9 timeout.tv_nsec = now.tv_usec * 1000; 10 retcode = 0; 11 while (x <= y && retcode != ETIMEDOUT) { 12 retcode = pthread_cond_timedwait(&cond, &mut, &timeout); 13 } 14 if (retcode == ETIMEDOUT) { 15 /* 发生超时 */ 16 } else { 17 /* 操作 x 和 y */ 18 } 19 pthread_mutex_unlock(&mut);
---
互斥锁一个明显的缺点是它只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线程间的同步。
条件变量的结构为pthread_cond_t,函数pthread_cond_init()被用来初始化一个条件变量。它的原型为:
extern int pthread_cond_init __P ((pthread_cond_t *__cond,__const pthread_condattr_t *__cond_attr));
其中cond是一个指向结构pthread_cond_t的指针,cond_attr是一个指向结构pthread_condattr_t的指针。结构pthread_condattr_t是条件变量的属性结构,和互斥锁一样可以用它来设置条件变量是进程内可用还是进程间可用,默认值是PTHREAD_ PROCESS_PRIVATE,即此条件变量被同一进程内的各个线程使用。注意初始化条件变量只有未被使用时才能重新初始化或被释放。释放一个条件变量的函数为pthread_cond_ destroy(pthread_cond_t cond)。
函数pthread_cond_wait()使线程阻塞在一个条件变量上。它的函数原型为:
extern int pthread_cond_wait __P ((pthread_cond_t *__cond,
pthread_mutex_t *__mutex));
线程解开mutex指向的锁并被条件变量cond阻塞。线程可以被函数pthread_cond_signal和函数pthread_cond_broadcast唤醒,但是要注意的是,条件变量只是起阻塞和唤醒线程的作用,具体的判断条件还需用户给出,例如一个变量是否为0等等,这一点从后面的例子中可以看到。线程被唤醒后,它将重新检查判断条件是否满足,如果还不满足,一般说来线程应该仍阻塞在这里,被等待被下一次唤醒。这个过程一般用while语句实现。
另一个用来阻塞线程的函数是pthread_cond_timedwait(),它的原型为:
extern int pthread_cond_timedwait __P ((pthread_cond_t *__cond,
pthread_mutex_t *__mutex, __const struct timespec *__abstime));
它比函数pthread_cond_wait()多了一个时间参数,经历abstime段时间后,即使条件变量不满足,阻塞也被解除。
函数pthread_cond_signal()的原型为:
extern int pthread_cond_signal __P ((pthread_cond_t *__cond));
它用来释放被阻塞在条件变量cond上的一个线程。多个线程阻塞在此条件变量上时,哪一个线程被唤醒是由线程的调度策略所决定的。要注意的是,必须用保护条件变量的互斥锁来保护这个函数,否则条件满足信号有可能在测试条件和调用pthread_cond_wait函数之间被发出,从而造成无限制的等待。下面是使用函数pthread_cond_wait()和函数pthread_cond_signal()的一个简单的例子。
1 pthread_mutex_t count_lock; 2 pthread_cond_t count_nonzero; 3 unsigned count; 4 decrement_count () { 5 pthread_mutex_lock (&count_lock); 6 while(count==0) 7 pthread_cond_wait( &count_nonzero, &count_lock); 8 count=count -1; 9 pthread_mutex_unlock (&count_lock); 10 } 11 12 increment_count(){ 13 pthread_mutex_lock(&count_lock); 14 if(count==0) 15 pthread_cond_signal(&count_nonzero); 16 count=count+1; 17 pthread_mutex_unlock(&count_lock); 18 }
count值为0时,decrement函数在pthread_cond_wait处被阻塞,并打开互斥锁count_lock。此时,当调用到函数increment_count时,pthread_cond_signal()函数改变条件变量,告知decrement_count()停止阻塞。
函数pthread_cond_broadcast(pthread_cond_t *cond)用来唤醒所有被阻塞在条件变量cond上的线程。这些线程被唤醒后将再次竞争相应的互斥锁,所以必须小心使用这个函数。
3) reader-writer lock
Reader-writer lock与mutex非常相似。r、RW lock有三种状态: 共享读取锁(shared-read), 互斥写入锁(exclusive-write lock),打开(unlock)。后两种状态与之前的mutex两种状态完全相同。
一个unlock的RW lock可以被某个线程获取R锁或者W锁。
如果被一个线程获得R锁,RW lock可以被其它线程继续获得R锁,而不必等待该线程释放R锁。但是,如果此时有其它线程想要获得W锁,它必须等到所有持有共享读取锁的线程释放掉各自的R锁。
如果一个锁被一个线程获得W锁,那么其它线程,无论是想要获取R锁还是W锁,都必须等待该线程释放W锁。
这样,多个线程就可以同时读取共享资源。而具有危险性的写入操作则得到了互斥锁的保护。
我们需要同步并发系统,这为程序员编程带来了难度。但是多线程系统可以很好的解决许多IO瓶颈的问题。比如我们监听网络端口。如果我们只有一个线程,那么我们必须监听,接收请求,处理,回复,再监听。如果我们使用多线程系统,则可以让多个线程监听。当我们的某个线程进行处理的时候,我们还可以有其他的线程继续监听,这样,就大大提高了系统的利用率。在数据越来越大,服务器读写操作越来越多的今天,这具有相当的意义。多线程还可以更有效地利用多CPU的环境。
(就像做饭一样,不断切换去处理不同的菜。)
本文中所使用的程序采用伪C的写法。不同的语言有不同的函数名(比如mutex_lock)。这里关注的是逻辑上的概念,而不是具体的实现和语言规范。
4) 信号灯
信号灯与互斥锁和条件变量的主要不同在于"灯"的概念,灯亮则意味着资源可用,灯灭则意味着不可用。如果说后两中同步方式侧重于"等待"操作,即资源不可用的话,信号灯机制则侧重于点灯,即告知资源可用;没有等待线程的解锁或激发条件都是没有意义的,而没有等待灯亮的线程的点灯操作则有效,且能保持灯亮状态。当然,这样的操作原语也意味着更多的开销。
信号灯的应用除了灯亮/灯灭这种二元灯以外,也可以采用大于1的灯数,以表示资源数大于1,这时可以称之为多元灯。
1. 创建和注销
POSIX信号灯标准定义了有名信号灯和无名信号灯两种,但LinuxThreads的实现仅有无名灯,同时有名灯除了总是可用于多进程之间以外,在使用上与无名灯并没有很大的区别,因此下面仅就无名灯进行讨论。
int sem_init(sem_t *sem, int pshared, unsigned int value)
这是创建信号灯的API,其中value为信号灯的初值,pshared表示是否为多进程共享而不仅仅是用于一个进程。LinuxThreads没有实现多进程共享信号灯,因此所有非0值的pshared输入都将使sem_init()返回-1,且置errno为ENOSYS。初始化好的信号灯由sem变量表征,用于以下点灯、灭灯操作。
int sem_destroy(sem_t * sem)
被注销的信号灯sem要求已没有线程在等待该信号灯,否则返回-1,且置errno为EBUSY。除此之外,LinuxThreads的信号灯注销函数不做其他动作。
2. 点灯和灭灯
int sem_post(sem_t * sem)
点灯操作将信号灯值原子地加1,表示增加一个可访问的资源。
int sem_wait(sem_t * sem) int sem_trywait(sem_t * sem)
sem_wait()为等待灯亮操作,等待灯亮(信号灯值大于0),然后将信号灯原子地减1,并返回。sem_trywait()为sem_wait()的非阻塞版,如果信号灯计数大于0,则原子地减1并返回0,否则立即返回-1,errno置为EAGAIN。
3. 获取灯值
int sem_getvalue(sem_t * sem, int * sval)
读取sem中的灯计数,存于*sval中,并返回0。
4. 其他
sem_wait()被实现为取消点,而且在支持原子"比较且交换"指令的体系结构上,sem_post()是唯一能用于异步信号处理函数的POSIX异步信号安全的API。
--------------
信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。当公共资源增加时,调用函数sem_post()增加信号量。只有当信号量值大于0时,才能使用公共资源,使用后,函数sem_wait()减少信号量。函数sem_trywait()和函数pthread_ mutex_trylock()起同样的作用,它是函数sem_wait()的非阻塞版本。下面逐个介绍和信号量有关的一些函数,它们都在头文件/usr/include/semaphore.h中定义。
信号量的数据类型为结构sem_t,它本质上是一个长整型的数。函数sem_init()用来初始化一个信号量。它的原型为:
extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));
sem为指向信号量结构的一个指针;pshared不为0时此信号量在进程间共享,否则只能为当前进程的所有线程共享;value给出了信号量的初始值。
函数sem_post( sem_t *sem )用来增加信号量的值。当有线程阻塞在这个信号量上时,调用这个函数会使其中的一个线程不再阻塞,选择机制同样是由线程的调度策略决定的。
函数sem_wait( sem_t *sem )被用来阻塞当前线程直到信号量sem的值大于0,解除阻塞后将sem的值减一,表明公共资源经使用后减少。函数sem_trywait ( sem_t *sem )是函数sem_wait()的非阻塞版本,它直接将信号量sem的值减一。
函数sem_destroy(sem_t *sem)用来释放信号量sem。
下面来看一个使用信号量的例子。在这个例子中,一共有4个线程,其中两个线程负责从文件读取数据到公共的缓冲区,另两个线程从缓冲区读取数据作不同的处理(加和乘运算)。
1 /* File sem.c */ 2 #include <stdio.h> 3 #include <pthread.h> 4 #include <semaphore.h> 5 #define MAXSTACK 100 6 int stack[MAXSTACK][2]; 7 int size=0; 8 sem_t sem; 9 10 /* 从文件1.dat读取数据,每读一次,信号量加一*/ 11 void ReadData1(void){ 12 FILE *fp=fopen("1.dat","r"); 13 while(!feof(fp)){ 14 fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]); 15 sem_post(&sem); 16 ++size; 17 } 18 fclose(fp); 19 } 20 21 /*从文件2.dat读取数据*/ 22 void ReadData2(void){ 23 FILE *fp=fopen("2.dat","r"); 24 while(!feof(fp)){ 25 fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]); 26 sem_post(&sem); 27 ++size; 28 } 29 fclose(fp); 30 } 31 32 /*阻塞等待缓冲区有数据,读取数据后,释放空间,继续等待*/ 33 void HandleData1(void){ 34 while(1){ 35 sem_wait(&sem); 36 printf("Plus:%d+%d=%d\n",stack[size][0],stack[size][1], 37 stack[size][0]+stack[size][1]); 38 --size; 39 } 40 } 41 42 void HandleData2(void){ 43 while(1){ 44 sem_wait(&sem); 45 printf("Multiply:%d*%d=%d\n",stack[size][0],stack[size][1], 46 stack[size][0]*stack[size][1]); 47 --size; 48 } 49 } 50 51 int main(void){ 52 pthread_t t1,t2,t3,t4; 53 sem_init(&sem,0,0); 54 pthread_create(&t1,NULL,(void *)HandleData1,NULL); 55 pthread_create(&t2,NULL,(void *)HandleData2,NULL); 56 pthread_create(&t3,NULL,(void *)ReadData1,NULL); 57 pthread_create(&t4,NULL,(void *)ReadData2,NULL); 58 /* 防止程序过早退出,让它在此无限期等待*/ 59 pthread_join(t1,NULL); 60 }
在Linux下,用命令gcc -lpthread sem.c -o sem生成可执行文件sem。事先编辑好数据文件1.dat和2.dat,假设它们的内容分别为1 2 3 4 5 6 7 8 9 10和 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 ,运行sem,得到如下的结果:
Multiply:-1*-2=2
Plus:-1+-2=-3
Multiply:9*10=90
Plus:-9+-10=-19
Multiply:-7*-8=56
Plus:-5+-6=-11
Multiply:-3*-4=12
Plus:9+10=19
Plus:7+8=15
Plus:5+6=11
从中可以看出各个线程间的竞争关系。而数值并未按原先的顺序显示出来,这是由于size这个数值被各个线程任意修改的缘故。这也往往是多线程编程要注意的问题。
五) 信号机制
在Linux的多线程中使用信号机制,与在进程中使用信号机制有着根本的区别,可以说是完全不同。在进程环境中,对信号的处理是,先注册信号处理函数,当信号异步发生时,调用处理函数来处理信号。它完全是异步的(我们完全不知到信号会在进程的那个执行点到来!)。然而信号处理函数的实现,有着许多的限制;比如有一些函数不能在信号处理函数中调用;再比如一些函数read、recv等调用时会被异步的信号给中断(interrupt),因此我们必须对在这些函数在调用时因为信号而中断的情况进行处理(判断函数返回时 enno 是否等于 EINTR)。
sigwait - wait for a signal #include int sigwait(const sigset_t *set, int *sig); Description The sigwait() function suspends execution of the calling thread until the delivery of one of the signals specified in the signal set set. The function accepts the signal (removes it from the pending list of signals), and returns the signal number in sig. The operation of sigwait() is the same as sigwaitinfo(2), except that: * sigwait() only returns the signal number, rather than a siginfo_t structure describing the signal. * The return values of the two functions are different. Return Value On success, sigwait() returns 0. On error, it returns a positive error number.
pthread_sigmask - examine and change mask of blocked signals #include int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset); Compile and link with -pthread. DESCRIPTION The pthread_sigmask() function is just like sigprocmask(2), with the difference that its use in multithreaded programs is explicitly specified by POSIX.1-2001. Other differences are noted in this page. For a description of the arguments and operation of this function, see sigprocmask(2). RETURN VALUE On success, pthread_sigmask() returns 0; on error, it returns an error number. NOTES A new thread inherits a copy of its creator's signal mask. (from man sigprocmask: ) The behavior of the call is dependent on the value of how, as follows. SIG_BLOCK The set of blocked signals is the union of the current set and the set argument. SIG_UNBLOCK The signals in set are removed from the current set of blocked signals. It is permissible to attempt to unblock a signal which is not blocked. SIG_SETMASK The set of blocked signals is set to the argument set. If oldset is non-NULL, the previous value of the signal mask is stored in oldset. If set is NULL, then the signal mask is unchanged (i.e., how is ignored), but the current value of the signal mask is nevertheless returned in oldset (if it is not NULL).
#include int pthread_kill(pthread_t thread, int sig); Compile and link with -pthread. DESCRIPTION The pthread_kill() function sends the signal sig to thread, another thread in the same process as the caller. The signal is asynchronously directed to thread. If sig is 0, then no signal is sent, but error checking is still performed; this can be used to check for the existence of a thread ID. RETURN VALUE On success, pthread_kill() returns 0; on error, it returns an error number, and no signal is sent. ERRORS ESRCH No thread with the ID thread could be found. EINVAL An invalid signal was specified.
1 #include <</SPAN>pthread.h> 2 #include <</SPAN>stdio.h> 3 #include <</SPAN>stdlib.h> 4 #include <</SPAN>unistd.h> 5 #include <</SPAN>signal.h> 6 #include <</SPAN>errno.h> 7 8 /* Simple error handling functions */ 9 10 #define handle_error_en(en, msg) \ 11 do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0) 12 13 static void * 14 sig_thread(void *arg) 15 { 16 sigset_t *set = (sigset_t *) arg; 17 int s, sig; 18 19 for (;;) { 20 s = sigwait(set, &sig); 21 if (s != 0) 22 handle_error_en(s, "sigwait"); 23 printf("Signal handling thread got signal %d\n", sig); 24 } 25 } 26 27 int 28 main(int argc, char *argv[]) 29 { 30 pthread_t thread; 31 sigset_t set; 32 int s; 33 34 /* 35 Block SIGINT; other threads created by main() will inherit 36 a copy of the signal mask. 37 */ 38 sigemptyset(&set); 39 sigaddset(&set, SIGQUIT); 40 sigaddset(&set, SIGUSR1); 41 s = pthread_sigmask(SIG_BLOCK, &set, NULL); 42 if (s != 0) 43 handle_error_en(s, "pthread_sigmask"); 44 s = pthread_create(&thread, NULL, &sig_thread, (void *) &set); 45 if (s != 0) 46 handle_error_en(s, "pthread_create"); 47 /* 48 Main thread carries on to create other threads and/or do 49 other work 50 */ 51 pause(); /* Dummy pause so we can test program */ 52 return 0; 53 }
1 digdeep@ubuntu:~/pthread/learnthread$ gcc -Wall -pthread -o pthread_sigmask pthread_sigmask.c 2 digdeep@ubuntu:~/pthread/learnthread$ ./pthread_sigmask & 3 [1] 4170 4 digdeep@ubuntu:~/pthread/learnthread$ kill -QUIT %1 5 digdeep@ubuntu:~/pthread/learnthread$ Signal handling thread got signal 3 6 digdeep@ubuntu:~/pthread/learnthread$ kill -USR1 %1 7 digdeep@ubuntu:~/pthread/learnthread$ Signal handling thread got signal 10 8 digdeep@ubuntu:~/pthread/learnthread$ kill -TERM %1 9 digdeep@ubuntu:~/pthread/learnthread$ 10 [1]+ Terminated ./pthread_sigmask 11 digdeep@ubuntu:~/pthread/learnthread$
由于LinuxThreads是在核外使用核内轻量级进程实现的线程,所以基于内核的异步信号操作对于线程也是有效的。但同时,由于异步信号总是实际发往某个进程,所以无法实现POSIX标准所要求的"信号到达某个进程,然后再由该进程将信号分发到所有没有阻塞该信号的线程中"原语,而是只能影响到其中一个线程。
--------
POSIX异步信号同时也是一个标准C库提供的功能,主要包括信号集管理(sigemptyset()、sigfillset()、sigaddset()、sigdelset()、sigismember()等)、信号处理函数安装(sigaction())、信号阻塞控制(sigprocmask())、被阻塞信号查询(sigpending())、信号等待(sigsuspend())等,它们与发送信号的kill()等函数配合就能实现进程间异步信号功能。LinuxThreads围绕线程封装了sigaction()何raise(),本节集中讨论LinuxThreads中扩展的异步信号函数,包括pthread_sigmask()、pthread_kill()和sigwait()三个函数。毫无疑问,所有POSIX异步信号函数对于线程都是可用的。
int pthread_sigmask(int how, const sigset_t *newmask, sigset_t *oldmask)
设置线程的信号屏蔽码,语义与sigprocmask()相同,但对不允许屏蔽的Cancel信号和不允许响应的Restart信号进行了保护。被屏蔽的信号保存在信号队列中,可由sigpending()函数取出。
int pthread_kill(pthread_t thread, int signo)
向thread号线程发送signo信号。实现中在通过thread线程号定位到对应进程号以后使用kill()系统调用完成发送。
int sigwait(const sigset_t *set, int *sig)
挂起线程,等待set中指定的信号之一到达,并将到达的信号存入*sig中。POSIX标准建议在调用sigwait()等待信号以前,进程中所有线程都应屏蔽该信号,以保证仅有sigwait()的调用者获得该信号,因此,对于需要等待同步的异步信号,总是应该在创建任何线程以前调用pthread_sigmask()屏蔽该信号的处理。而且,调用sigwait()期间,原来附接在该信号上的信号处理函数不会被调用。
如果在等待期间接收到Cancel信号,则立即退出等待,也就是说sigwait()被实现为取消点。
六) 其他同步方式
除了上述讨论的同步方式以外,其他很多进程间通信手段对于LinuxThreads也是可用的,比如基于文件系统的IPC(管道、Unix域Socket等)、消息队列(Sys.V或者Posix的)、System V的信号灯等。只有一点需要注意,LinuxThreads在核内是作为共享存储区、共享文件系统属性、共享信号处理、共享文件描述符的独立进程看待的。
总结:
1).互斥锁必须总是由给它上锁的线程解锁,信号量的挂出即不必由执行过它的等待操作的同一进程执行。一个线程可以等待某个给定信号灯,而另一个线程可以挂出该信号灯。
2).互斥锁要么锁住,要么被解开(二值状态,类型二值信号量)
3).由于信号量有一个与之关联的状态(它的计数值),信号量挂出操作总是被记住。然而当向一个条件变量发送信号时,如果没有线程等待在该条件变量上,那么该信号将丢失。
4).互斥锁是为了上锁而设计的,条件变量是为了等待而设计的,信号灯即可用于上锁,也可用于等待,因而可能导致更多的开销和更高的复杂性。
--------------
Linux系统中的进程通信方式主要以下几种:
同一主机上的进程通信方式
* UNIX进程间通信方式: 包括管道(PIPE), 有名管道(FIFO), 和信号(Signal)
* System V进程通信方式:包括信号量(Semaphore), 消息队列(Message Queue), 和共享内存(Shared Memory)
网络主机间的进程通信方式
* RPC: Remote Procedure Call 远程过程调用
* Socket: 当前最流行的网络通信方式, 基于TCP/IP协议的通信方式.
各自的特点:
- 管道(PIPE):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系(父子进程)的进程间使用。另外管道传送的是无格式的字节流,并且管道缓冲区的大小是有限的(管道缓冲区存在于内存中,在管道创建时,为缓冲区分配一个页面大小)。
- 有名管道 (FIFO): 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
- 信号(Signal): 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
- 信号量(Semaphore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
- 消息队列(Message Queue):消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 共享内存(Shared Memory ):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
- 套接字(Socket): 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机间的进程通信。
Linux系统中的线程通信方式主要以下几种:
* 锁机制:包括互斥锁、条件变量、读写锁
互斥锁提供了以排他方式防止数据结构被并发修改的方法。
使用条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
读写锁允许多个线程同时读共享数据,而对写操作是互斥的。
* 信号量机制(Semaphore):包括无名线程信号量和命名线程信号量
* 信号机制(Signal):类似进程间的信号处理