线程同步——条件变量

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); 只能定时到 19701100:00:01(早已经过去) 

正确用法:

time_t cur = time(NULL); 获取当前时间。

struct timespec t; 定义timespec 结构体变量t

t.tv_sec = cur+1; 定时1pthread_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); 

以上是关于线程同步——条件变量的主要内容,如果未能解决你的问题,请参考以下文章

线程同步

信号量互斥量同步变量条件变量和事件变量

线程资源同步——互斥量和条件变量

(转载)pThreads线程 线程同步--条件变量

[C++11 多线程同步] --- 条件变量的那些坑条件变量信号丢失和条件变量虚假唤醒(spurious wakeup)

[C++11 多线程同步] --- 条件变量的那些坑条件变量信号丢失和条件变量虚假唤醒(spurious wakeup)