LV025-条件变量
本文主要是线程同步——条件变量的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
一、条件变量
条件变量是线程可用的另一种同步机制。条件变量用于自动阻塞线程,知道某个特定事件发生或某个条件满足为止,通常情况下,条件变量是和互斥锁一起搭配使用的,这是因为条件的检测是在互斥锁的保护下进行的,也就是说条件本身是由互斥锁保护的,线程在改变条件状态之前必须先锁住互斥锁,不然就可能引发线程不安全的问题。
条件变量就相当于生产者和消费者模的式,生产者这边负责生产产品、而消费者负责消费产品,对于消费者来说,没有产品的时候只能等待产品出来,有产品就使用它。
二、基本操作
1. 条件变量初始化
1.1 pthread_cond_init()
1.1.1 函数说明
在linux下可以使用man pthread_cond_init命令查看该函数的帮助手册。
/* Compile and link with -pthread. */
#include <pthread.h>
/* man 手册中的声明 */
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
/* 一些资料中的声明 */
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);额......,😥跟上边一样,使用常见的声明格式把,毕竟参数都是一样的,但是常见的还是更容易理解一些。
【函数说明】该函数是以动态方式初始化一个条件变量。
【函数参数】
cond:pthread_cond_t *类型,指向需要进行初始化操作的条件变量对象。attr:const pthread_condattr_t *类型,指向一个pthread_condattr_t类型对象,该对象用于描述条件变量的属性,若将参数attr设置为NULL,则表示条件变量的属性设置为默认值,在这种情况下其实就等价于PTHREAD_COND_INITIALIZER这种方式初始化,而不同之处在于,使用宏不进行错误检查。
【返回值】int类型,成功返回0;失败将返回一个非0的错误码。
【使用格式】一般情况下基本使用格式如下:
/* 需要包含的头文件 */
#include <pthread.h>
/* 至少应该有的语句 */
pthread_cond_t cond;/* 定义全局变量 */
pthread_cond_init(&cond, NULL);/* 初始化条件变量 */
/* 或者 */
pthread_cond_t *cond = malloc(sizeof(pthread_cond_t)); /* 定义为全局指针变量 */
pthread_cond_init(cond, NULL);/* 初始化条件变量 */【注意事项】
(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命令查看该宏的定义格式。
/* Compile and link with -pthread. */
#include <pthread.h>
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; /* cond为条件变量名称,符合变量标识符定义规则即可。*/【函数说明】该宏用于使用默认属性初始化一个条件变量。
**【定义原型】**该宏定义在pthread.h文件中,可以在终端中使用以下命令查看pthread.h文件位置:
locate pthread.h # 若出现locate 命令未安装之类的,按照提示安装即可定义形式如下:
/* Conditional variable handling. */
#define PTHREAD_COND_INITIALIZER { { 0, 0, 0, 0, 0, (void *) 0, 0, 0 } }【返回值】none
【使用格式】一般情况下基本使用格式如下:
/* 需要包含的头文件 */
#include <pthread.h>
/* 至少应该有的语句 */
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;【注意事项】
(1)PTHREAD_COND_INITIALIZER宏已经携带了条件变量默认属性。
(2)只适用于在定义的时候就直接进行初始化,对于其它情况则不能使用这种方式。
1.2.2 使用实例
暂无。
2. 通知和等待
2.1 pthread_cond_signal()
2.1.1 函数说明
在linux下可以使用man 3 pthread_cond_signal命令查看该宏的定义格式。
/* Compile and link with -pthread. */
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);【函数说明】函数向条件变量发送信号(至少唤醒一个线程)。
【函数参数】
cond:pthread_cond_t *类型,指向已经初始化过的条件变量对象。
【返回值】int类型,成功返回0;失败将返回一个非0的错误码。
【使用格式】一般情况下基本使用格式如下:
/* 需要包含的头文件 */
#include <pthread.h>
/* 至少应该有的语句 */
pthread_cond_t cond;/* 定义全局变量 */
pthread_cond_init(&cond, NULL);/* 初始化条件变量 */
pthread_cond_signal(&cond);/* 向条件变量发送信号 */【注意事项】对阻塞于pthread_cond_wait()的多个线程,pthread_cond_signal()函数至少能唤醒一个线程。
2.1.2 使用实例
暂无。
2.2 pthread_cond_broadcast()
2.2.1 函数说明
在linux下可以使用man pthread_cond_broadcast命令查看该函数的帮助手册。
/* Compile and link with -pthread. */
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);【函数说明】该函数向条件变量发送信号(唤醒所有线程,这叫线程的惊群效应)。
【函数参数】
cond:pthread_cond_t *类型,指向已经初始化过的条件变量对象。
【返回值】int类型,成功返回0;失败将返回一个非0的错误码。
【使用格式】一般情况下基本使用格式如下:
/* 需要包含的头文件 */
#include <pthread.h>
/* 至少应该有的语句 */
pthread_cond_t cond;/* 定义全局变量 */
pthread_cond_init(&cond, NULL);/* 初始化条件变量 */
pthread_cond_broadcast(&cond);/* 向条件变量发送信号 */【注意事项】
(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命令查看该函数的帮助手册。
/* Compile and link with -pthread. */
#include <pthread.h>
/* man 手册中的声明 */
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
/* 一些资料中的声明 */
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);【函数说明】程序中使用条件变量,当判断某个条件不满足时,调用此函数将线程设置为等待状态(阻塞)。
【函数参数】
cond:pthread_cond_t *类型,指向需要进行初始化操作的条件变量对象。mutex:pthread_mutex_t *类型,指向一个互斥锁对象;前边说过的,条件变量通常是和互斥锁一起使用,因为条件的检测(条件检测通常是需要访问共享资源的)是在互斥锁的保护下进行的,也就是说条件本身是由互斥锁保护的。
Tips:函数内部对互斥锁的使用规则如下:
在
pthread_cond_wait()函数内部会对参数mutex所指定的互斥锁进行操作,通常情况下,条件判断以及pthread_cond_wait()函数调用均在互斥锁的保护下,也就是说,在此之前线程已经对互斥锁加锁了。调用
pthread_cond_wait()函数时,调用者把互斥锁传递给函数,函数会自动把调用线程放到等待条件的线程列表上,然后将互斥锁解锁;当pthread_cond_wait()被唤醒返回时,会再次锁住互斥锁。
【返回值】int类型,成功返回0;失败将返回一个非0的错误码。
【使用格式】一般情况下基本使用格式如下:
/* 需要包含的头文件 */
#include <pthread.h>
/* 至少应该有的语句 */
pthread_cond_t cond;/* 定义全局变量 */
pthread_mutex_t mutex;/* 定义全局变量 */
pthread_cond_init(&cond, NULL);/* 初始化条件变量 */
pthread_mutex_init(&mutex, NULL);/* 初始化互斥锁 */
pthread_mutex_lock(&mutex);
/* 等待信号的条件 */
while(<表达式>)
{
pthread_cond_wait(&cond, &mutex);
}
/* 中间为共享资源的访问操作 */
pthread_mutex_unlock(&mutex);【注意事项】条件变量并不保存状态信息,只是传递应用程序状态信息的一种通讯机制。如果调用pthread_cond_signal()和pthread_cond_broadcast()向指定条件变量发送信号时,若无任何线程等待该条件变量,这个信号也就会自动消失。
2.3.2 使用实例
暂无。
2.4 pthread_cond_timedwait()
2.4.1 函数说明
在linux下可以使用man pthread_cond_timedwait命令查看该函数的帮助手册。
/* Compile and link with -pthread. */
#include <pthread.h>
/* man 手册中的声明 */
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);还是这样一个函数,我们直接忽略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命令查看该函数的帮助手册。
/* Compile and link with -pthread. */
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);【函数说明】该函数用于销毁一个不再使用的条件变量(条件变量对象实际上变成了未初始化的对象)。
【函数参数】
cond:pthread_cond_t *类型,指向已经初始化过的条件变量对象。
【返回值】int类型,用成功时返回0;失败将返回一个非0值的错误码。
【使用格式】一般情况下基本使用格式如下:
/* 需要包含的头文件 */
#include <pthread.h>
/* 至少应该有的语句 */
pthread_cond_destroy(&cond);/* 具体看定义的方式,是变量还是指针变量,指针变量则不需要&符号。*/【注意事项】
(1)对没有进行初始化的条件变量进行销毁,也将可能会导致未定义行为。
(2)对某个条件变量而言,仅当没有任何线程等待它时,将其销毁才是最安全的。
(3)经pthread_cond_destroy()销毁的条件变量,可以再次调用pthread_cond_init()对其进行重新初始化。
(4)高版本的Ubuntu中需要确保条件变量没有线程在使用的情况下才能销毁,否则会导致程序卡死,至少我使用的Ubuntu21.04(64位)就是这样的。
4.1.2 使用实例
暂无。
三、条件变量的属性
条件变量与前边的各种锁一样,都可以设置属性,调用pthread_cond_init()函数初始化条件变量时,可以设置条件变量的属性,通过参数attr指定。条件变量包括两个属性:进程共享属性和时钟属性。由于还没有用到过,暂时先提一下,后边用到了再补充笔记。
四、使用例程
例程这里,我们模拟消费者与生产者关系。
1. 使用实例1
本实例是一个生产者,一个消费者,也就是说,这个例程有一个消费者线程,一个生产者线程,通过条件变量来实现同步。
1.1 测试源码
#include <stdio.h>
#include <pthread.h>/* pthread_create pthread_exit pthread_cancel pthread_self */
#include <unistd.h> /* sleep */
#include <string.h> /* strerror */
#include <stdlib.h> /* malloc */
#define macro_judge 1 /* macro为1时进行Head是否为空的判断,测试消费者线程会怎样的情况,为1时,不会有信号丢失,为0时,会有信号丢失 */
struct product
{
int num; /* 产品编号 */
struct product * next;/* 下一个产品 */
};
struct product * Head = NULL;/* 定义一个产品线的头 */
/* 静态方式初始化互斥锁 */
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
/* 初始化条件变量 */
pthread_cond_t hasProduct = PTHREAD_COND_INITIALIZER;
/* 相关函数实现 */
void *producer(void *arg);
void *consumer(void *arg);
/* 主函数 */
int main(int argc, char *argv[])
{
int ret;
pthread_t tid1, tid2;
/* 1. 创建线程 */
ret = pthread_create(&tid1, NULL, producer, NULL);
printf("Create producer thread ,ret=%d,tid1=%lu\n", ret, tid1);
sleep(5);/* 先生产几件产品,然后再开启消费者线程 */
ret = pthread_create(&tid2, NULL, consumer, (void *)1);
printf("Create consumer thread ,ret=%d,tid2=%lu\n", ret, tid2);
sleep(1);
while(1)
{
sleep(1);
}
return 0;
}
/* 生产者线程 */
void *producer(void *arg)
{
/* 定义一个产品的结构体指针变量 */
struct product * pd;
int i = 0;
/* 设置线程分离,结束后自动回收 */
pthread_detach(pthread_self());
printf("This is a producer thread test!\n");
/* 生产产品 */
while(1)
{
/* 申请内存 */
pd = (struct product *)malloc(sizeof(struct product));
/* 判断内存是否申请成功 */
if(pd == NULL)
{
printf("product malloc failed!\n");
pthread_exit("product thread malloc failed!");
}
pd->num = i++;
printf("product %d is producing!\n", pd->num);
pthread_mutex_lock(&lock); /* 获取互斥锁 */
pd->next = Head;
Head = pd;
/* 发送产品生产完成信号 */
pthread_cond_signal(&hasProduct);
pthread_mutex_unlock(&lock);/* 解除互斥锁 */
sleep(1);
}
/* 退出线程 */
pthread_exit("producer thread return!");
}
/* 消费者线程 */
void *consumer(void *arg)
{
/* 设置线程分离,结束后自动回收 */
pthread_detach(pthread_self());
printf("This is a consumer thread test!\n");
/* 定义一个产品的结构体指针变量 */
struct product * pd;
while(1)
{
pthread_mutex_lock(&lock);/* 获取互斥锁 */
/* 等待产品生产完成信号 */
#if macro_judge == 1
while(Head == NULL)
#endif
{
/* 没有资源的时候进行等待,等待的时候会解除互斥锁,然后进入休眠,信号到了再重新获取互斥锁 */
pthread_cond_wait(&hasProduct,&lock);
}
pd = Head;
Head = pd->next;
printf("%d,Take product %d\n", (int)arg, pd->num);
free(pd);/* 释放内存 */
pthread_mutex_unlock(&lock);/* 解除互斥锁 */
}
pthread_exit("consumer thread return!");
}程序中的宏macro_judge用于决定在消费者线程中是否添加产品余量的while循环:
- 若
macro_judge为0,则不进行判断,直接进行等信号到来,但是这样,消费者线程开启之前的信号就全部丢失了。 - 若
macro_judge为1,则加入判断,这样即便是消费者线程开启之前到来的信号也会被处理掉。
1.2 macro_judge == 0
在终端执行以下命令编译程序:
gcc test.c -Wall -l pthread # 生成可执行文件 a.out 有一个警告,是线程创建传参造成的,正常现象
./a.out # 执行可执行程序然后,终端会有以下信息显示:
Create producer thread ,ret=0,tid1=139928004916800
This is a producer thread test!
product 0 is producing!
product 1 is producing!
product 2 is producing!
product 3 is producing!
product 4 is producing!
Create consumer thread ,ret=0,tid2=139927996524096
This is a consumer thread test!
product 5 is producing!
1,Take product 5
product 6 is producing!
1,Take product 6
# 后边的省略 ......根据结果发现,消费者线程启动前发出的商品信号丢失了,消费者线程并未获取到。
1.3 macro_judge == 1
在终端执行以下命令编译程序:
gcc test.c -Wall -l pthread # 生成可执行文件 a.out 有一个警告,是线程创建传参造成的,正常现象
./a.out # 执行可执行程序然后,终端会有以下信息显示:
Create producer thread ,ret=0,tid1=140716674655808
This is a producer thread test!
product 0 is producing!
product 1 is producing!
product 2 is producing!
product 3 is producing!
product 4 is producing!
Create consumer thread ,ret=0,tid2=140716666263104
This is a consumer thread test!
1,Take product 4
1,Take product 3
1,Take product 2
1,Take product 1
1,Take product 0
product 5 is producing!
1,Take product 5
product 6 is producing!
1,Take product 6
product 7 is producing!
1,Take product 7
# 后边的省略 ......经打印结果发现,所有的信号都被消费者线程获取到了。
2. 使用实例2
本实例是一个生产者,三个消费者,也就是说,这个例程有三个消费者线程,一个生产者线程,通过条件变量来实现同步。
#include <stdio.h>
#include <pthread.h>/* pthread_create pthread_exit pthread_cancel pthread_self */
#include <unistd.h> /* sleep */
#include <string.h> /* strerror */
#include <stdlib.h> /* malloc */
struct product
{
int num; /* 产品编号 */
struct product * next;/* 下一个产品 */
};
struct product * Head = NULL;/* 定义一个产品线的头 */
#define macro_judge 0 /* 0,不进行Head == NULL的判断;1,Head == NULL */
#define broadcast 1 /* 0,信号只能唤醒一个线程;1,信号将唤醒所有线程 */
/* 静态方式初始化互斥锁 */
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
/* 初始化条件变量 */
pthread_cond_t hasProduct = PTHREAD_COND_INITIALIZER;
/* 相关函数实现 */
int testConditionVariable(void);
void *producer(void *arg);
void *consumer(void *arg);
/* 主函数 */
int main(int argc, char *argv[])
{
int ret;
pthread_t tid1, tid2, tid3, tid4;
/* 1. 创建线程 */
ret = pthread_create(&tid1, NULL, producer, NULL);
printf("Create producer thread ,ret=%d,tid1=%lu\n", ret, tid1);
ret = pthread_create(&tid2, NULL, consumer, (void *)1);
printf("Create consumer thread ,ret=%d,tid2=%lu\n", ret, tid2);
ret = pthread_create(&tid3, NULL, consumer, (void *)2);
printf("Create consumer thread ,ret=%d,tid3=%lu\n", ret, tid3);
ret = pthread_create(&tid4, NULL, consumer, (void *)3);
printf("Create consumer thread ,ret=%d,tid4=%lu\n", ret, tid4);
sleep(1);
while(1)
{
sleep(1);
}
return 0;
}
/* 生产者线程 */
void *producer(void *arg)
{
/* 定义一个产品的结构体指针变量 */
struct product * pd;
int i = 0;
/* 设置线程分离,结束后自动回收 */
pthread_detach(pthread_self());
printf("This is a producer thread test!\n");
/* 生产产品 */
while(1)
{
/* 申请内存 */
pd = (struct product *)malloc(sizeof(struct product));
/* 判断内存是否申请成功 */
if(pd == NULL)
{
printf("product malloc failed!\n");
pthread_exit("product thread malloc failed!");
}
pd->num = i++;
printf("product %d is producing!\n", pd->num);
pthread_mutex_lock(&lock); /* 获取互斥锁 */
pd->next = Head;
Head = pd;
/* 发送产品生产完成信号 */
#if broadcast == 1
pthread_cond_broadcast(&hasProduct);/* 注意此函数发送的信号会被所有等待信号的线程得到 */
#else
pthread_cond_signal(&hasProduct);/* 注意此函数发送的信号只会被一个线程得到 */
#endif
pthread_mutex_unlock(&lock);/* 解除互斥锁 */
sleep(1);
}
/* 退出线程 */
pthread_exit("producer thread return!");
}
/* 消费者线程 */
void *consumer(void *arg)
{
/* 设置线程分离,结束后自动回收 */
pthread_detach(pthread_self());
printf("This is a consumer thread test!\n");
/* 定义一个产品的结构体指针变量 */
struct product * pd;
while(1)
{
pthread_mutex_lock(&lock);/* 获取互斥锁 */
/* 等待产品生产完成信号 */
#if macro_judge == 1
while(Head == NULL)
#endif
{
/* 没有资源的时候进行等待,等待的时候会解除互斥锁,然后进入休眠,信号到了再重新获取互斥锁 */
pthread_cond_wait(&hasProduct,&lock);
}
pd = Head;
Head = pd->next;
printf("%d,Take product %d\n", (int)arg, pd->num);
free(pd);/* 释放内存 */
pthread_mutex_unlock(&lock);/* 解除互斥锁 */
}
pthread_exit("consumer thread return!");
}这里有两个宏,一共有四种情况测试,这里就不再放自己的测试情况了,只分析一下两个宏定义的作用:
程序中的宏macro_judge用于决定在消费者线程中是否添加产品余量的while循环:
| macro_judge为0 | 不进行Head == NULL的判断,直接进行等信号到来,但是这样,消费者线程开启之前的信号就全部丢失了。 |
| macro_judge为1 | 进行Head == NULL的判断,这样即便是消费者线程开启之前到来的信号也会被处理掉。 |
| broadcast为0 | 则将信号广播给1个线程,只会有1个线程会被唤醒。 |
| broadcast为1 | 则将信号广播给所有线程,所有线程都会被唤醒开始抢夺信号。 |