线程同步——条件变量
Posted Shemesz
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程同步——条件变量相关的知识,希望对你有一定的参考价值。
一、条件变量介绍
条件变量是线程可用的一种同步机制。 条件变量给多个线程提供了一个回合的场所,条件变量与互斥量一起使用,允许线程以无竞争的方式等待特定的条件发生。
条件本身是由互斥量保护的。 线程在改变条件状态之前必须先锁住互斥量。其他线程在获得互斥量之前都不会察觉到这种改变,因为互斥量必须在锁定以后才能计算体条件。
在使用条件变量之前,必须进行初始化。 由pthread_cond_t数据类型表示的条件变量可以通过两种方式初始化,可以把常量PTHREAD_COND_INITIALIZER赋给静态分配的条件变量,但是如果条件变量是动态分配的,则需要使用pthread_cond_init函数对他进行初始化。
示例说明 :
假设进程里有两个线程一个主线程一个线程,程序要求执行流程如下
第一步,在主线程执行任务A;
第二步,在子线程执行任务B;
第三步,在主线程执行任务C;
第四步,程序退出
创建全局变量giWorkFlag用来线程之间通信;全局互斥体g_mutex用来保护g_iWorkFlag。
// 全局变量
pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t g_cond = PTHREAD_COND_INITIALIZER;
int g_iWorkFlag = 0;
总体框架如下
运行步骤:
第1步:创建子线程任务
第2步:主线程执行工作A
第3步:执行完工作A后,用条件变量,通知子线程执行工作B
第4步:守候条件变量,等执行工作B…
第5步:得到主线程通知后,在此执行工作B
第6步:执行完工作B后,用条件变量,通知主线程执行工作C
第7步:执行完工作B,子线程退出
第8步:守候条件变量,等执行工作C…
第9步:主线程执行工作C
第10步:等待子线程退出(其实早已退出),退出主程序
二、运行代码
// 主线程
int main(void)
{
printf ("%d MAIN: START. // WORK_STEP=%d\\n", (int)time(NULL), g_iWorkFlag);
// 第1步:创建子线程任务
pthread_t tid;
pthread_create(&tid, NULL, func_thread, NULL);
// 第2步:主线程执行工作A
printf ("%d MAIN: WORKING A... // WORK_STEP=%d\\n", (int)time(NULL), g_iWorkFlag);
sleep(2); // 这是工作A
printf ("%d MAIN: WORK A DONE. // WORK_STEP=%d\\n", (int)time(NULL), g_iWorkFlag);
// 第3步:执行完工作A后,用条件变量,通知子线程执行工作B
pthread_mutex_lock(&g_mutex);
g_iWorkFlag = 1; // 1=>B
pthread_cond_signal(&g_cond);
pthread_mutex_unlock(&g_mutex);
// 第8步:守候条件变量,等执行工作C...
pthread_mutex_lock(&g_mutex);
while (g_iWorkFlag != 2)
pthread_cond_wait(&g_cond, &g_mutex);
pthread_mutex_unlock(&g_mutex);
// 第9步:主线程执行工作C
printf ("%d MAIN: WORKING C... // WORK_STEP=%d\\n", (int)time(NULL), g_iWorkFlag);
sleep(2); // 这是工作C
printf ("%d MAIN: WORK C DONE. // WORK_STEP=%d\\n", (int)time(NULL), g_iWorkFlag);
// 第10步:等待子线程退出(其实早已退出),退出主程序
pthread_join(tid, NULL);
printf ("%d MAIN: EXIT. // WORK_STEP=%d\\n", (int)time(NULL), g_iWorkFlag);
return 0;
}
// 子线程
static void *func_thread(void *arg)
{
printf ("%d SUB_THREAD: ENTER... // WORK_STEP=%d\\n", (int)time(NULL), g_iWorkFlag);
// 第4步:守候条件变量,等执行工作B...
pthread_mutex_lock(&g_mutex);
while (g_iWorkFlag != 1)
pthread_cond_wait(&g_cond, &g_mutex);
pthread_mutex_unlock(&g_mutex);
// 第5步:得到主线程通知后,在此执行工作B
printf ("%d SUB_THREAD: WORKING B... // WORK_STEP=%d\\n", (int)time(NULL), g_iWorkFlag);
sleep(2); // 这是工作B
printf ("%d SUB_THREAD: WORK B DONE. // WORK_STEP=%d\\n", (int)time(NULL), g_iWorkFlag);
// 第6步:执行完工作B后,用条件变量,通知主线程执行工作C
pthread_mutex_lock(&g_mutex);
g_iWorkFlag = 2; // 2=>C
pthread_cond_signal(&g_cond);
pthread_mutex_unlock(&g_mutex);
// 第7步:执行完工作B,子线程退出
printf ("%d SUB_THREAD: EXIT. // WORK_STEP=%d\\n", (int)time(NULL), g_iWorkFlag);
return 0;
}
运行结果
上边的源码已经通过注释和打印描述得很清楚,这里不再追溯。只有pthread_cond_wait()比较难于理解,这里着重解释一下,即pthread_cond_wait()内部会执行如下2步:
第1步:解锁,阻塞着等待条件变量。
第2步:被条件变量唤醒,加锁,返回。
为了保护pthread_cond_wait()内部阻塞等待之前的“对g_cond的操作1”和阻塞等待之后的“对g_cond的操作2”,以及为了与pthread_cond_wait()内部的解锁与加锁对应上,所以需要在外部前边加锁,后边解锁。
注意事项:
为什么要用while(g_iWorkFlag != XXX)呢?这是因为pthread_cond_wait()可能被虚假唤醒(Spurious Wakeups),pthread_cond_signal()可能会唤醒多个线程上的条件变量;而条件变量本身不能精确指定流程的信息,所以需要通过一个全局变量g_iWorkFlag来指示流程细节状态。如果有的线程被唤醒,但流程状态不是自己所期待的,则通过while循环重新调用pthread_cond_wait()阻塞等待。而g_mutex既保护了条件变量,也保护了g_iWorkFlag,一举两得。
三、条件变量相关API
- pthread_cond_init()函数 功能:初始化一个条件变量
- pthread_cond_wait()函数 功能:阻塞等待一个条件变量
- pthread_cond_timedwait()函数 功能:限时等待一个条件变量
- pthread_cond_signal()函数 功能:唤醒至少一个阻塞在条件变量上的线程
- pthread_cond_broadcast()函数 功能:唤醒全部阻塞在条件变量上的线程
- pthread_cond_destroy()函数 功能:销毁一个条件变量
- 以上6 个函数的返回值都是:成功返回0, 失败直接返回错误号。
pthread_cond_t 类型,其本质是一个结构体。为简化理解,应用时可忽略其实现细节,简单当成整数看待。如:
pthread_cond_t cond; 变量cond只有两种取值1、0。
<1> 初始化一个条件变量
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
参2:attr表条件变量属性,通常为默认值,传NULL即可
也可以使用静态初始化的方法,初始化条件变量:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
<2> 阻塞等待一个条件变量
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
函数作用:阻塞等待条件变量cond(参1)满足
释放已掌握的互斥锁(解锁互斥量)相当于pthread_mutex_unlock(&mutex);
1.2.两步为一个原子操作。
3.当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex);
<3> 限时等待一个条件变量
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
参3: 参看man sem_timedwait函数,查看struct timespec结构体。
struct timespec {
time_t tv_sec; /* seconds */ 秒
long tv_nsec; /* nanosecondes*/ 纳秒
}
形参abstime:绝对时间。
如:time(NULL)返回的就是绝对时间。而alarm(1)是相对时间,相对当前时间定时1秒钟。
struct timespec t = {1, 0};
pthread_cond_timedwait (&cond, &mutex, &t); 只能定时到 1970年1月1日 00:00:01秒(早已经过去)
正确用法:
time_t cur = time(NULL); 获取当前时间。
struct timespec t; 定义timespec 结构体变量t
t.tv_sec = cur+1; 定时1秒
pthread_cond_timedwait (&cond, &mutex, &t); 传参 参APUE.11.6线程同步条件变量小节
<4> 唤醒至少一个阻塞在条件变量上的线程
int pthread_cond_signal(pthread_cond_t *cond);
<5> 唤醒全部阻塞在条件变量上的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
<6> 销毁一个条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
以上是关于线程同步——条件变量的主要内容,如果未能解决你的问题,请参考以下文章
[C++11 多线程同步] --- 条件变量的那些坑条件变量信号丢失和条件变量虚假唤醒(spurious wakeup)
[C++11 多线程同步] --- 条件变量的那些坑条件变量信号丢失和条件变量虚假唤醒(spurious wakeup)