LV05-03-线程同步-05-条件变量
本文主要是线程同步——条件变量的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
Windows | windows11 |
Ubuntu | Ubuntu16.04的64位版本 |
VMware® Workstation 16 Pro | 16.2.3 build-19376536 |
SecureCRT | Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日 |
开发板 | 正点原子 i.MX6ULL Linux阿尔法开发板 |
uboot | NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03) |
linux内核 | linux-4.15(NXP官方提供) |
STM32开发板 | 正点原子战舰V3(STM32F103ZET6) |
点击查看本文参考资料
参考方向 | 参考原文 |
--- | --- |
点击查看相关文件下载
--- | --- |
一、条件变量
条件变量是线程可用的另一种同步机制。条件变量用于自动阻塞线程,知道某个特定事件发生或某个条件满足为止,通常情况下,条件变量是和互斥锁一起搭配使用的,这是因为条件的检测是在互斥锁的保护下进行的,也就是说条件本身是由互斥锁保护的,线程在改变条件状态之前必须先锁住互斥锁,不然就可能引发线程不安全的问题。
条件变量就相当于生产者和消费者模的式,生产者这边负责生产产品、而消费者负责消费产品,对于消费者来说,没有产品的时候只能等待产品出来,有产品就使用它。
二、基本操作
1. 条件变量初始化
1.1 pthread_cond_init()
1.1.1 函数说明
在linux
下可以使用man pthread_cond_init
命令查看该函数的帮助手册。
1 | /* Compile and link with -pthread. */ |
额……,😥跟上边一样,使用常见的声明格式把,毕竟参数都是一样的,但是常见的还是更容易理解一些。
【函数说明】该函数是以动态方式初始化一个条件变量。
【函数参数】
cond
:pthread_cond_t *
类型,指向需要进行初始化操作的条件变量对象。attr
:const pthread_condattr_t *
类型,指向一个pthread_condattr_t
类型对象,该对象用于描述条件变量的属性,若将参数attr
设置为NULL
,则表示条件变量的属性设置为默认值,在这种情况下其实就等价于PTHREAD_COND_INITIALIZER
这种方式初始化,而不同之处在于,使用宏不进行错误检查。
【返回值】int
类型,成功返回0
;失败将返回一个非0
的错误码。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】
(1)在使用条件变量之前必须对条件变量进行初始化操作,使用PTHREAD_COND_INITIALIZER
宏或者函数pthread_cond_init()
都行;
(2) 对已经初始化的条件变量再次进行初始化,将可能会导致未定义行为。
1.1.2 使用实例
暂无。
1.2 PTHREAD_COND_INITIALIZER
1.2.1 函数说明
在linux
下可以使用man 3 pthread_cond_init
命令查看该宏的定义格式。
1 | /* Compile and link with -pthread. */ |
【函数说明】该宏用于使用默认属性初始化一个条件变量。
【定义原型】该宏定义在pthread.h
文件中,可以在终端中使用以下命令查看pthread.h
文件位置:
1 | locate pthread.h # 若出现locate 命令未安装之类的,按照提示安装即可 |
定义形式如下:
1 | /* Conditional variable handling. */ |
【返回值】none
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】
(1)PTHREAD_COND_INITIALIZER
宏已经携带了条件变量默认属性。
(2)只适用于在定义的时候就直接进行初始化,对于其它情况则不能使用这种方式。
1.2.2 使用实例
暂无。
2. 通知和等待
2.1 pthread_cond_signal()
2.1.1 函数说明
在linux
下可以使用man 3 pthread_cond_signal
命令查看该宏的定义格式。
1 | /* Compile and link with -pthread. */ |
【函数说明】函数向条件变量发送信号(至少唤醒一个线程)。
【函数参数】
cond
:pthread_cond_t *
类型,指向已经初始化过的条件变量对象。
【返回值】int
类型,成功返回0
;失败将返回一个非0
的错误码。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】对阻塞于pthread_cond_wait()
的多个线程,pthread_cond_signal()
函数至少能唤醒一个线程。
2.1.2 使用实例
暂无。
2.2 pthread_cond_broadcast()
2.2.1 函数说明
在linux
下可以使用man pthread_cond_broadcast
命令查看该函数的帮助手册。
1 | /* Compile and link with -pthread. */ |
【函数说明】该函数向条件变量发送信号(唤醒所有线程,这叫线程的惊群效应)。
【函数参数】
cond
:pthread_cond_t *
类型,指向已经初始化过的条件变量对象。
【返回值】int
类型,成功返回0
;失败将返回一个非0
的错误码。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】
(1)对阻塞于pthread_cond_wait()
的多个线程,pthread_cond_broadcast()
函数能唤醒所有线程。
(2)使用pthread_cond_broadcast()
函数总能产生正确的结果,唤醒所有等待状态的线程,但函数pthread_cond_signal()
会更为高效,因为它只需确保至少唤醒一个线程即可。
(3)当调用pthread_cond_broadcast()
同时唤醒所有线程时,由于互斥锁也只能被某一线程锁住,其它线程获取锁失败又会陷入阻塞。
2.2.2 使用实例
暂无。
2.3 pthread_cond_wait()
2.3.1 函数说明
在linux
下可以使用man pthread_cond_wait
命令查看该函数的帮助手册。
1 | /* Compile and link with -pthread. */ |
【函数说明】程序中使用条件变量,当判断某个条件不满足时,调用此函数将线程设置为等待状态(阻塞)。
【函数参数】
cond
:pthread_cond_t *
类型,指向需要进行初始化操作的条件变量对象。mutex
:pthread_mutex_t *
类型,指向一个互斥锁对象;前边说过的,条件变量通常是和互斥锁一起使用,因为条件的检测(条件检测通常是需要访问共享资源的)是在互斥锁的保护下进行的,也就是说条件本身是由互斥锁保护的。
点击查看函数内部对互斥锁的使用规则
在pthread_cond_wait()
函数内部会对参数mutex
所指定的互斥锁进行操作,通常情况下,条件判断以及pthread_cond_wait()
函数调用均在互斥锁的保护下,也就是说,在此之前线程已经对互斥锁加锁了。
调用pthread_cond_wait()
函数时,调用者把互斥锁传递给函数,函数会自动把调用线程放到等待条件的线程列表上,然后将互斥锁解锁;当pthread_cond_wait()
被唤醒返回时,会再次锁住互斥锁。
【返回值】int
类型,成功返回0
;失败将返回一个非0
的错误码。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】条件变量并不保存状态信息,只是传递应用程序状态信息的一种通讯机制。如果调用pthread_cond_signal()
和pthread_cond_broadcast()
向指定条件变量发送信号时,若无任何线程等待该条件变量,这个信号也就会自动消失。
2.3.2 使用实例
暂无。
2.4 pthread_cond_timedwait()
2.4.1 函数说明
在linux
下可以使用man pthread_cond_timedwait
命令查看该函数的帮助手册。
1 | /* Compile and link with -pthread. */ |
还是这样一个函数,我们直接忽略restrict
的存在好吧😆,反正都一样。
【函数说明】程序中使用条件变量,当判断某个条件不满足时,调用此函数将线程设置为等待状态(阻塞),等待条件变量的同时可以设置等待超时,超时就会退出,不会再继续等待。
【函数参数】
cond
:pthread_cond_t *
类型,指向已经初始化过的条件变量对象。mutex
:pthread_mutex_t *
类型,指向一个互斥锁对象。abstime
:struct timespec *
类型,表示等待的时间,它是一个绝对值,也就是距离1970-1-1
日的时间值,而不是一个时间段。比如说当前时间为2022-5-1 12:00:00.000
,我们想通过这个函数设置最大超时为2500ms
,那么就需要设置abstime
时间为2022-5-1 12:00:02.500
。
【返回值】int
类型,成功返回0
;失败将返回一个非0
的错误码。
【使用格式】none
【注意事项】此函数时间设定个人感觉还是很复杂的,暂时还木有用到过,后边用到了再补充。
2.4.2 使用实例
暂无。
3. 判断条件
使用条件变量,都会有与之相关的判断条件,通常情况下,会涉及到一个或多个共享变量。条件的判断必须使用while
循环,而不是if
语句,这是一种通用的设计原则:当线程从pthread_cond_wait()
返回时,并不能确定判断条件的状态,应该立即重新检查判断条件,如果条件不满足,那就继续休眠等待。
从pthread_cond_wait()
返回后,并不能确定判断条件是真还是假,其理由如下:
(1)当有多于一个线程在等待条件变量时,任何线程都有可能会率先醒来获取互斥锁,率先醒来获取到互斥锁的线程可能会对共享变量进行修改,进而改变判断条件的状态。
(2)可能会发出虚假的通知。
4. 销毁条件变量
定义了条件变量之后,当不再需要条件变量时,应该将其销毁。
4.1 pthread_cond_destroy()
4.1.1 函数说明
在linux
下可以使用man pthread_cond_destroy
命令查看该函数的帮助手册。
1 | /* Compile and link with -pthread. */ |
【函数说明】该函数用于销毁一个不再使用的条件变量(条件变量对象实际上变成了未初始化的对象)。
【函数参数】
cond
:pthread_cond_t *
类型,指向已经初始化过的条件变量对象。
【返回值】int
类型,用成功时返回0
;失败将返回一个非0
值的错误码。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】
(1)对没有进行初始化的条件变量进行销毁,也将可能会导致未定义行为。
(2)对某个条件变量而言,仅当没有任何线程等待它时,将其销毁才是最安全的。
(3)经pthread_cond_destroy()
销毁的条件变量,可以再次调用pthread_cond_init()
对其进行重新初始化。
(4)高版本的Ubuntu
中需要确保条件变量没有线程在使用的情况下才能销毁,否则会导致程序卡死,至少我使用的Ubuntu21.04(64位)
就是这样的。
4.1.2 使用实例
暂无。
三、条件变量的属性
条件变量与前边的各种锁一样,都可以设置属性,调用pthread_cond_init()
函数初始化条件变量时,可以设置条件变量的属性,通过参数attr
指定。条件变量包括两个属性:进程共享属性和时钟属性。由于还没有用到过,暂时先提一下,后边用到了再补充笔记。
四、使用例程
例程这里,我们模拟消费者与生产者关系。
1. 使用实例1
本实例是一个生产者,一个消费者,也就是说,这个例程有一个消费者线程,一个生产者线程,通过条件变量来实现同步。
点击查看实例
1 |
|
程序中的宏macro_judge
用于决定在消费者线程中是否添加产品余量的while
循环:
- 若
macro_judge
为0
,则不进行判断,直接进行等信号到来,但是这样,消费者线程开启之前的信号就全部丢失了。 - 若
macro_judge
为1
,则加入判断,这样即便是消费者线程开启之前到来的信号也会被处理掉。
在终端执行以下命令编译程序:
1 | gcc test.c -Wall -l pthread # 生成可执行文件 a.out 有一个警告,是线程创建传参造成的,正常现象 |
然后,终端会有以下信息显示:
1 | Create producer thread ,ret=0,tid1=139928004916800 |
根据结果发现,消费者线程启动前发出的商品信号丢失了,消费者线程并未获取到。
在终端执行以下命令编译程序:
1 | gcc test.c -Wall -l pthread # 生成可执行文件 a.out 有一个警告,是线程创建传参造成的,正常现象 |
然后,终端会有以下信息显示:
1 | Create producer thread ,ret=0,tid1=140716674655808 |
经打印结果发现,所有的信号都被消费者线程获取到了。
2. 使用实例2
本实例是一个生产者,三个消费者,也就是说,这个例程有三个消费者线程,一个生产者线程,通过条件变量来实现同步。
点击查看实例
1 |
|
这里有两个宏,一共有四种情况测试,这里就不再放自己的测试情况了,只分析一下两个宏定义的作用:
程序中的宏macro_judge
用于决定在消费者线程中是否添加产品余量的while
循环:
macro_judge为0 | 不进行Head == NULL的判断,直接进行等信号到来,但是这样,消费者线程开启之前的信号就全部丢失了。 |
macro_judge为1 | 进行Head == NULL的判断,这样即便是消费者线程开启之前到来的信号也会被处理掉。 |
程序中的宏broadcast
用于决定生产者线程中发出信号后,唤醒多少线程:
broadcast为0 | 则将信号广播给1个线程,只会有1个线程会被唤醒。 |
broadcast为1 | 则将信号广播给所有线程,所有线程都会被唤醒开始抢夺信号。 |
【注意事项】broadcast = 1
时,若macro_judge = 0
,则会发生段错误,若macro_judge = 1
时正常。 发生原因:例如生产2
件产品时,信号广播给所有线程,三个线程都会抢这两个信号,总有一个抢不到,若不进行判断,访问一个空指针,自然会出现内存访问错误 从而报段错误。