线程的互斥与同步

目录

1、基础概念

2、数据不加保护时被多个线程访问会造成什么后果呢?

3、如何解决上文中的问题呢?(加锁,即线程互斥)

3.1、方案一:静态分配锁

3.2、方案二:动态分配锁

3.3、持有锁的线程会被切换吗?会导致出现问题吗?

3.4、加锁和解锁的原理

4、重入和线程安全

5、死锁

5.1、产生死锁的四个必要条件

6、线程同步

6.1、为什么要有条件变量

6.2、如何使用条件变量呢?

6.2.1、初始化条件变量

6.2.2、等待条件变量(包括伪唤醒、条件变量的使用规范)

6.2.3、当临界资源就绪时通知线程(唤醒线程)

6.2.4、销毁条件变量


1、基础概念

1.临界资源:多线程执行流共享的资源就叫做临界资源。

2.临界区:每个线程内部,访问临界资源的代码,就叫做临界区。

3.互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。

4.原子性:具有原子性的操作是不会被任何调度机制打断的,该操作只有两态,要么未开始,要么完成。换句话说就是执行了具有原子性的操作后,一定会产生结果,没有中间态。比如--(自减)操作就不是原子操作,因为自减操作对应三条汇编指令。第一是load :将共享变量ticket从内存加载到寄存器中。第二是update : 更新寄存器里面的值,执行减一操作。第三是store :将新值从寄存器写回共享变量ticket的内存地址。由于CPU执行每一条汇编时都随时有可能被切走,所以--操作是可能存在中间态的,所以--操作不具有原子性。

2、数据不加保护时被多个线程访问会造成什么后果呢?

大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个 线程,其他线程无法获得这种变量。 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享完成线程之间的交互。 但多个线程并发地操作共享变量会带来一些问题,什么问题呢?请看下图的实验。

答案:因为全局变量等数据是所有线程共享的,所以每个线程都可以访问它,而当数据不加保护时被多个线程访问就会造成数据紊乱,详情看下图的实验。

代码如下

线程的互斥与同步_第1张图片

运行结果

线程的互斥与同步_第2张图片

问题:如上图中,把实验的情景模拟成抢飞机票,全局变量tickets=10000,全局变量是所有线程共享的,票有一万张,创建了3个线程,每个线程可以将它当成乘客。票的值应该只为1到10000,可运行结果中有tickets的值等于0和-1,说明多卖了票,为什么会这样呢?

答案:首先需要知道一个线程A的时间片到时,不管线程A正在执行什么,OS都会将线程A放在CPU寄存器中的上下文数据转移到线程A的PCB中保存起来,方便之后切换回来时继续执行线程A的代码,然后进行线程切换。上图情景中,假如此时正在执行线程A的代码,目前tickets的值为1,因为tickets的值大于0,所以通过了循环的条件,刚执行到usleep这一行时,OS发现线程A的时间片到了,就将线程A切换,然后CPU执行线程B的代码,仅管线程A通过了循环的条件,但线程A中并没有执行到ticket--这一行,所以线程B中进行循环判断时,ticket的值仍为1,1大于0,所以条件通过,然后假如线程B的时间片有点长,ticket--被执行了,此时ticket的值就为0,然后OS发现线程B的时间片到了,将线程B又重新切换成线程A,线程A中是通过了循环的条件的,此时再将tickets--,tickets的值就变成了-1。这就是经典的【因为时序不一致,导致多个线程在并发访问数据时,数据不一致】的问题。

问题:如果有时看到连续几行都是同一个线程在printf打印,即乘客连续抢票的情况,这该如何解释呢?

答案:因为在每个线程被切换之前,即每个线程的时间片中,该线程能够执行printf语句的次数是未知的,所以甚至有可能会看到同一个线程连续打印,即乘客连续抢票的情况。

问题:除了上面的问题,还会造成其他隐患吗?

答案:会有极小概率造成数据紊乱的问题,依然拿上面的实验举例。首先要知道ticket--这一行代码并不是看上去的这么简单,它分为三步,第一步是将ticket的值从内存读取到CPU的寄存器中,第二步是CPU进行减减运算,最后一步是将运算结果从CPU的寄存器写回到内存,而不管正在执行这三步中的哪一步,随时都有可能进行线程切换。如下图,假如进程中有线程A和线程B,此时线程A将ticket的值从内存读取到CPU的寄存器中,值为10000,因为线程A运气较差,所以还没来得及ticket--时线程A就被切换成线程B了,线程A在切换前会将CPU的上下文数据保存在线程A的PCB中,其中ticket的值仍为10000。线程切换后,在线程B中,因为线程B的运气较好,时间片比较长,所以循环执行了5000次tickets--,此时寄存器中的tickets的值为5000,在第5000次将tickets的值从CPU寄存器写回进内存后,内存中的tickets也为5000了,此时OS发现线程B的时间片也到了,然后再次从线程B切换回线程A,切换时将之前保存在线程A的PCB中的上下文数据重新加载进CPU寄存器里,而在PCB中的上下文数据中的tickets值是10000,所以加载进CPU寄存器并进行tickets--运算后,寄存器中的tickets的值为9999,此时再次写回进内存,那么内存中tickets的值就从之前的5000变成了9999,那么再从线程A切换到线程B时,线程B读取到的tickets的值就是9999,相当于之前线程B白忙活了一场,这就是数据紊乱。

线程的互斥与同步_第3张图片

3、如何解决上文中的问题呢?(加锁,即线程互斥)

最常见的方法是通过互斥锁保护数据,避免数据紊乱,但互斥锁会导致进程的效率降低。Linux上提供的这把锁也叫互斥量,创建互斥锁有两种方案。

注意所有线程必须使用同一把锁,所以互斥锁最好是全局变量,如果非得定义在局部,那也有办法,详情见下文方案二。如果一个线程不加锁就访问临界资源,它就是一种错误的编码方式。

3.1、方案一:静态分配锁

25e11649ba2141d3950f68d6b451b158.png线程的互斥与同步_第4张图片

1.首先创建并初始化互斥锁。上图红框中pthread_mutex是一个结构体,其中有很多字段,变量mtx是我们定义的一把互斥锁,红框右半部分是一个宏,用于初始化互斥锁。

线程的互斥与同步_第5张图片线程的互斥与同步_第6张图片

2.然后用上图红框处的pthread_mutex_lock函数加锁,参数就是定义的锁的地址。注意只有一个线程可以获取这把锁,没有获取这把锁的线程会在加锁函数所在的那一行阻塞等待,只有获取锁的线程将锁释放后,其他线程才可以从阻塞态恢复,从而继续执行代码,这段临界区中的代码的执行方式被称为串行化执行。也正是因为其他线程被阻塞,上文中才会说互斥锁会导致进程的效率降低,所以在加锁时,加锁的粒度越小越好,即如果可以将代码写在临界区外,最好就这么做。再次强调一定要注意释放锁,不然没有获取到锁的线程就没法玩了。当有若干个线程同时申请互斥锁,但没有竞争到互斥锁,那么该线程调用pthread_ lock会陷入阻塞(执行流被挂起),等待互斥锁解锁。

线程的互斥与同步_第7张图片

3.最后用上图红框处的函数解锁。注意使用方案一静态分配锁时,不需要在解锁后销毁互斥锁。

完整流程代码如下

线程的互斥与同步_第8张图片

运行结果如下

线程的互斥与同步_第9张图片

加锁后tickets的值不再出现0或者-1了,运行正确。但为什么所有的tickets的值都是同一个线程printf打印的呢?或者说为什么所有的票都被同一个线程给抢了?答案:这个完全是看调度器的心情,可以认为该线程,假如叫线程A的优先级更高,每次循环执行代码时,刚刚把线程A占用的锁给解掉,其他线程终于可以获取锁时,还没拿到锁,OS又进行线程切换,从其他线程切换回线程A,此时A又将锁给占用了,最终导致所有的票都被同一个线程给抢了。如果非要看到除了线程A以外的线程拿到票,如下图实验,可以在pthread_mutex_unlock解锁函数的下面使用sleep,而且sleep的时间完全随机,如果线程A刚好随机到一个超过了时间片的数字,OS发现线程A的时间片到了切换成其他线程,由于线程A在切换前已经解锁了,所以其他线程就有机会抢到锁了,那么就可以看到除了线程A以外的线程拿到了票。但注意这里所有的票都被一个线程拿到并没有发生错误,只是不合理。

代码如下

线程的互斥与同步_第10张图片

运行结果如下

线程的互斥与同步_第11张图片

 如上图,这时就不再只有thread one了,而出现了one、two和three。

3.2、方案二:动态分配锁

线程的互斥与同步_第12张图片

如果在局部定义互斥锁,并且没有用static修饰该锁,那么必须使用上图红框处的函数创建并初始化互斥锁,不能使用上面的方案一创建互斥锁。这里补充一点,不管static变量定义在局部还是全局,它都是全局变量。创建锁的函数的第一个参数为定义的互斥锁的地址,第二个参数不用管,设置成nullptr即可,方案二在新线程中加锁和解除锁的方式和上面的方案一相同。

注意以方案二创建互斥锁时,使用完互斥锁后,要得记得用下图函数销毁互斥锁。用于线程等待的函数和销毁互斥锁的函数的执行是没有严格的先后顺序的,只是对应的两个函数都必须执行。销毁互斥锁(互斥量)时需要注意:1.使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量,即方案一采用静态分配的锁不需要销毁。2.不要销毁一个已经加锁的互斥量。3.已经销毁的互斥量,要确保后面不会有线程再尝试加锁。线程的互斥与同步_第13张图片

方案二的完整流程的代码如下

下图中互斥锁定义在局部,但在初始化锁、加锁、解锁、销毁锁时传入了mtx的地址,也就是锁的地址,所以保证了所有线程使用的互斥锁是同一把。

线程的互斥与同步_第14张图片

3.3、持有锁的线程会被切换吗?会导致出现问题吗?

答案:持有锁的线程A在时间片到了或者其他条件满足时也会被切换,但不会出现问题,因为线程A是持有锁的,即使线程A被切换成线程B,线程B想要执行临界区中的代码,即位于加锁函数后的代码时也必须申请锁,但由于线程A加锁后还没有解锁,即锁被线程A占用了,所以线程B是无法申请成功的,所以就保证了其他线程进入不了临界区,就保证了临界区中数据的一致性。

3.4、加锁和解锁的原理

线程的互斥与同步_第15张图片

前言

经过上面的例子大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题,所以为了实现互斥锁操作,大多数体系结构都提供了swap或exchange汇编指令,该指令的作用是把寄存器和内存单元的数据交换,由于只有一条指令,所以保证了原子性,即使是多处理器平台访问内存,总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

如上图是加锁函数pthread_mutex_lock的汇编代码,【movb $0,%al】表示将数据0移动到寄存器al中,【xchgb %al,mutex】表示将al寄存器中的值与内存中互斥锁的表示锁是否被某个线程占用的字段的值交换,该字段的值为1则表示锁未被申请,为0则表示已被申请,为了方便叙述,这里把该字段称为字段Z。下面我们模拟两个线程都想要加锁,从而竞争互斥锁的场景。首先线程A执行第一行代码后,此时al寄存器中的值为0,不巧的是,线程A想要执行第二行代码时被切走了,所以要在线程A的PCB中保存al的0。此时线程B执行第一行代码,al寄存器中的值又变成了0,继续执行第二行代码,交换al和字段Z的值,那么al现在值为1,字段Z的值为0。执行到这里时,无论线程B是否在此时被切换,线程A都拿不到这个1了,因为1只有一份,线程B在被切换成线程A时,OS会将al中的1放进线程B的PCB中。假设此时从线程B切换回线程A,继续执行线程A的代码,将线程A的PCB中的0放进al中后,线程A继续执行第二行代码,将al的0和字段Z的0交换,然后执行if判断,al的值为0,发现0不大于0,条件不满足,所以线程A就被挂起等待了,再从线程A切换到线程B,将线程B的PCB中的1放进al中,然后执行代码进行if判断,al的值为1,1大于0,条件满足,此时没有被阻塞,而是return 0函数退出了,线程B就可以执行临界区的代码了。在这个场景中也就是线程B竞争锁成功了。可以发现所谓的竞争锁的过程就是看哪个进程拿到了1,因为最开始只有字段Z的值是1,而【xchgb %al,mutex】是交换而非拷贝,所以1是只有一份的,所以有一个线程拿到就代表其他线程都拿不到。

线程的互斥与同步_第16张图片

如上图就是获取了互斥锁的线程访问完临界资源后解锁的过程,将1放到字段Z中,然后唤醒处于阻塞等待的其他线程,最后退出函数即可成功给获取了锁的线程解锁。而如下图,处于阻塞等待的线程被唤醒后,会执行的语句为goto lock,即重新申请锁。

线程的互斥与同步_第17张图片

明白了加锁和解锁的原理后,回看之前说的【持有锁的线程也会随时进行线程切换,但切换时是带着锁走的,线程切换后,其他线程依然访问不了临界区,因为他们依然申请不了锁】就可以深刻理解了,本质上是因为他们拿不到位于获取到了锁的线程的PCB中的1,所以在申请锁的函数中出不来,所以无法退出函数从而执行位于函数外的临界区的代码。

4、重入和线程安全

1.当有一个执行流还没有执行完一个函数时就有其他的执行流再次进入该函数,我们把这个动作称之为重入。一个函数在被重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数,可以看到重入这种概念是针对函数的。上文中的实验就是多个线程重入地执行同一个函数,因为上文中线程调用的函数在不加锁时执行就会导致数据紊乱,所以该函数就是一个不可重入函数。

2.线程安全:多个线程并发同一段代码时如果出现了不同的结果,这就叫出现了线程安全的问题,常见的线程安全问题就是没有锁保护的情况下对全局变量或者静态变量进行操作。可以看到线程安全这种概念是针对线程的。注意如果一个全局变量出现在函数A和函数B中,即使多个线程没有重入访问某个函数,而是一个线程访问A,一个线程访问B,此时还是会出现线程安全的问题。

可重入与线程安全联系
1.函数是可重入的,那就是线程安全的。
2.函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
3.如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

可重入与线程安全区别
1.可重入函数是线程安全函数的一种。
2.如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。所以线程安全不一定是可重入的,而可重入函数则一定是线程安全的。

5、死锁

线程中有时不止一把锁,假如线程A申请锁1成功,线程B申请锁2成功,那么假如因为编写代码的人忘记了之前申请了哪些锁,此时在线程A中申请锁2,线程B中申请锁1,因为锁1和锁2都已经被占用了,所以线程A和线程B就都会处于阻塞等待,导致代码无法继续向下推进,双方都在等待对方线程先释放之前申请的锁,但两个线程因为都被阻塞了,所以都无法释放锁,最后导致死锁问题。

那么只有一个线程并且只有一把锁时,有可能发生死锁吗?答案:有可能。比如用户在申请锁1后并且由于忘记了之前申请过锁1,再次申请时,就会因为锁1已被占用,导致第二次申请失败,导致线程被阻塞挂起了,同样的,该线程也无法被别人或者自己唤醒,所以最后导致死锁问题。

5.1、产生死锁的四个必要条件

只有同时满足四个条件才会产生死锁,如下:

1.互斥条件:一个资源每次只能被一个执行流使用。
2.请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。
3.不剥夺条件 : 一个执行流已获得的资源,在末使用完之前,不能强行剥夺。
4.循环等待条件 : 若干执行流之间形成一种头尾相接的循环等待资源的关系。

所以可以发现只要破坏四个必要条件的其中一个,死锁也就被解开了。

6、线程同步

线程同步的概念:

1.上文中的两组抢票实验中,没有在加锁函数下sleep的一组实验的运行结果虽然没有错,互斥锁保护了临界资源,但所有临界资源都被同一个线程抢占显然是不合理的,所以就引入了线程同步,线程同步不仅用于合理分配资源,还用于控制多个线程按照一定的顺序访问资源,本质上合理分配资源就是靠控制线程的执行顺序完成的。

2.处理多线程问题时,假如有多个线程访问同一个变量,并且某些线程还想修改这个变量,这时候我们就需要用到线程同步。线程同步其实就是一种等待机制,多个同时需要访问此变量的线程进入这个变量的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。

问题产生的背景:我们在访问临界资源前,首先要检测临界资源是否存在,但给临界资源做检测本质也是在访问临界资源。因为临界资源是所有线程共享的,线程A检测临界资源的同时,线程B是有可能修改临界资源的,一边检测一边修改当然不行,所以对临界资源做检测的代码也是需要在加锁函数和解锁函数之间的。就拿上文中的抢票举例子,这里会将上文抢票的环节稍作修改。想要抢票,就得先看还有没有票,这就称为访问临界资源前需要先检测临界资源,上文中检查有没有票的语句即 if 判断就是在加锁函数和解锁函数之间的,这就称为对临界资源做检测的代码也是需要在加锁函数和解锁函数之间的。如果目前票被抢完了,没有票就称为临界资源不就绪,假如票会在一段时间后重新产生,但因为我们不知道一段时间是多久,所以用常规的方式想要在资源就绪时,即有票时就立刻抢到票,就一定得频繁检测临界资源(票),又因为对临界资源做检测的代码得位于加锁函数和解锁函数之间,所以一定还得频繁的申请锁和释放锁。前面这些动作都是有成本的,如果资源一直不就绪,还反复的检测资源是否就绪,相当于了花了很大的成本,但没有任何收益。

问题:那有没有一种方法,让线程检测到资源不就绪时不再继续反复检测,而是等待资源就绪,当资源就绪时就通知对应的线程,让它来进行资源的访问呢?

答案:有,可以通过条件变量,pthread_cond_t类型的变量就叫做条件变量。pthread_cond_t类型本质是一个结构体类型。为简化理解,应用时可忽略其实现细节,简单当成整数看待,比如现在有条件变量pthread_cond_t  cond,那么变量cond只有两种取值,为1或者0,代表条件变量就绪或者条件变量不就绪。详情见下文。

6.1、为什么要有条件变量

条件变量适用于这样的情形:两个线程完成的任务之间有明确的先后顺序,比如说A线程负责删除链表中的一个元素,B线程负责往链表中插入一个元素。对于一个空链表,只有B线程先执行过了,A线程能够执行,所以必须要控制线程的执行顺序。

条件变量(cond)和锁(mutex)是紧密相关的,锁的使用场景是:这件事同时只有一个人能做,我抢到锁就进去做了,我做完再给下一个人做。这时就加个锁,保证某些变量同时只被一个线程操作。
什么情况下要用到条件变量呢?首先这件事还是只能一个人做,所以还要用锁。但线程抢到锁了后,发现还要等待一些条件满足才能做。这时怎么办呢?难道抢到锁的线程要不停检查这个条件吗?如果是这样,那消耗会非常高。那每次检查完sleep一下可以吗?也不行,因为sleep的时间少了,消耗依然很高,而多了又有延迟,不能即时发现条件满足了。而且你抢到锁一直检查,别人也拿不到锁了。为了解决这个问题,就引入了条件变量。抢到锁的线程发现条件未满足时先释放锁,然后使用pthread_cond_wait()挂起自己,这时别的线程也能获取锁进入临界区,别的线程发现条件也不满足那同样pthread_cond_wait()阻塞挂起自己,直到条件满足后,这时需要通知其他的线程了,由其他方调用,一般是主线程使用pthread_cond_broacast函数唤醒全部挂起的线程或者使用pthread_cond_signal函数唤醒一个线程,即通知这些挂起的线程说:条件好了,快来做吧,这时,第一个抢到锁的线程进去就可以干活了。

6.2、如何使用条件变量呢?

注意条件变量一定是配合锁使用的,没有锁,那就不会使用条件变量,毕竟和条件变量有关的接口的参数都是需要传锁的地址的。

6.2.1、初始化条件变量

首先使用条件变量前得初始化,初始化条件变量的方法有两种。

第一种:如果条件变量是全局变量或者局部静态变量,则用下图红框处的方法初始化条件变量。

线程的互斥与同步_第18张图片

第二种:如果条件变量是局部的,则用下图红框处的pthread_cond_init函数将它初始化,和上面创建锁后要销毁锁一样,这里创建完条件变量后也要记得用函数pthread_cond_destroy将条件变量销毁。第一个参数cond表示要初始化的条件变量,第二个参数attr不用管,设置成nullptr即可,在两个参数前的restrict是一个修饰符,有兴趣自行搜索了解。函数调用成功时返回0,失败返回错误码errno。

线程的互斥与同步_第19张图片

6.2.2、等待条件变量(包括伪唤醒、条件变量的使用规范)

线程的互斥与同步_第20张图片

上文中说临界资源不就绪时,也就是票被抢完了并且没有重新生成新的票时,可以让线程不再继续反复检测临界资源是否就绪,而是陷入阻塞,等待主线程通知时再恢复运行。用于让新线程陷入阻塞的函数如上图。第一个参数cond就是条件变量的地址,第二个参数mutex为互斥锁的地址。函数调用成功时返回0,失败返回错误码errno。当CPU在线程中成功调用pthread_cond_wait函数时,当前线程会先释放持有的锁,(这也是为什么在调用该函数时要给它的第二个参数传入一个互斥锁的地址,不然找不到需要释放的锁)然后当前线程会立刻进入阻塞,等到当前线程被其他线程调用pthread_cond_signal等等函数唤醒,即被通知条件变量已就绪时,当前线程才会在其他线程释放锁时有机会获取互斥锁,也就是说当前线程并不是立即恢复运行的,当前线程虽然不需要等待条件变量,但当前线程还需要等待其他线程释放锁,其他线程释放锁后,当前线程还需要竞争调度器的资源,因为其他线程如果优先被调度,那锁就会被其他线程抢占,当前线程就会继续等待锁,只有在锁没有被抢占并且当前线程还被调度时,wait函数才可以帮当前线程自动获取锁,此时等待条件变量成功,并且占有了锁,就可以继续访问临界资源了。所以简而言之,该函数就是用于让当前线程释放持有的锁,然后让当前线程在某个条件变量上陷入阻塞,等到当前线程被其他线程唤醒而且其他线程释放了锁,当前线程才有机会获取锁,何时有机会呢?等到其他线程没有抢占到锁,并且已被唤醒正在等待锁的当前线程被调度起来执行wait函数内部的获取锁的逻辑的时候,这时函数pthread_cond_wait才会自动帮当前线程重新获取互斥锁。

注意只要是一个函数,就有可能调用失败,wait函数也不例外,调用失败后会返回错误码,然后继续执行线程后序的位于临界区的代码,这相当于线程被伪唤醒了,所以一般要循环调用wait函数,如下图,队列中的数据就是当前情景下的临界资源,如果队列为空,则表示临界资源不就绪,则返回true并调用wait函数,如果调用成功则陷入阻塞,等待被其他线程唤醒,如果其他线程通知了当前线程,也就是把条件变量修改成【有效】,则表示临界资源已经就绪,此时队列理论上就不为空,所以判断返回false,结束循环,然后访问临界资源。如果函数调用失败,则代码向下推进,继续循环判断此时队列是否为空,发现为空,也就表明当前线程是被伪唤醒的,此时就继续调用wait函数。通过这样的访问临界资源的规范可以保证访问临界资源时,资源一定是就绪的。之前说过检测临界资源(当前情景中队列里的数据就是临界资源)本质就是在访问临界资源,所以检测临界资源一定要在加锁和解锁之间,下图代码也确实符合这一规范。综上所述,使用pthread_cond_wait函数时必须套上循环,如下图。

pthread_cond_wait函数存在虚假唤醒的情况,这是多线程编程中的一个重要概念。虚假唤醒指的是在等待条件变量时,即使条件变量的条件尚未满足,线程也可能会被唤醒。这并不是因为条件实际上已经满足,而是因为一些其他的线程调用了pthread_cond_signal或pthread_cond_broadcast函数,或者由于一些信号中断了pthread_cond_wait函数的等待。虚假唤醒可能发生的原因包括:1、信号中断:线程在等待条件变量时可能会被信号中断,这会导致pthread_cond_wait函数提前返回。这是因为线程可以注册信号处理程序,如果信号触发,它将中断等待。2、竞争条件:多个线程在同时等待条件变量,当其他线程调用pthread_cond_signal或pthread_cond_broadcast时,它们中的一个或多个线程将被唤醒,但并不一定是因为条件实际上已经满足。为了正确使用pthread_cond_wait,您通常需要在while循环内等待条件,而不仅仅是在if语句内等待,以处理虚假唤醒。

线程的互斥与同步_第21张图片

6.2.3、当临界资源就绪时通知线程(唤醒线程)

线程的互斥与同步_第22张图片

模拟一种情景:进程中不止一个线程在等待条件变量就绪,或者说在等待临界资源就绪,因为条件变量是用户在临界资源就绪时设置成【就绪状态】的,上图中的pthread_cond_broadcast函数就可以用于通知所有的线程条件变量已经就绪,即把所有在等待条件变量就绪的线程唤醒。而函数pthread_cond_signal用于通知一个正在等待条件变量就绪的线程。两个函数如何实现通知的功能呢?本质就是修改条件变量中的某个字段,修改完成后,条件变量也就变成【就绪状态】了。函数调用成功时返回0,失败返回错误码errno。注意当线程A没有调用pthread_cond_wait函数陷入阻塞时,如果其他线程调用pthread_cond_signal函数试图将线程A唤醒,线程A会自动忽略其他线程的通知信息(唤醒信息)。

6.2.4、销毁条件变量

d688cae9f42842e992b6c6bb1117cd75.png

上图函数就用于销毁条件变量,第一个参数cond表示要销毁的条件变量。函数调用成功时返回0,失败返回-1。注意如果初始化条件变量是采用下图红框处的方法完成的,则不需要使用上图中的函数销毁条件变量。如果初始化条件变量是使用pthread_cond_init函数完成的,则需要用上图中的函数销毁条件变量。线程的互斥与同步_第23张图片

你可能感兴趣的:(Linux,linux)