【Linux】线程同步技术之二:条件变量(condition variable)

1.线程同步有两种技术

第一种是互斥锁,确保只有一个线程对临界资源的访问,但是由于线程的并发性(<Linux_Unix系统编程手册(上)>P514:调用pthread_create()后,应用程序无从确定系统接着会调度哪一个线程来使用CPU资源,在多处理器系统中,多个线程可能会在不同CPU上同时执行),无法确定线程运行的先后顺序

第二种是条件变量,(<Linux_Unix系统编程手册(上)>P529:条件变量允许一个线程就某个共享变量或其他共享资源的状态变化通知其他线程,并让其他线程阻塞于这一通知),条件变量的使用或许可以给线程间运行的先后顺序提供一些思路,在处理线程运行问题时(比如某一个线程需要等待,直到另一个线程达到某种条件后才继续运行),可以不仅仅依靠全部变量去控制

2.pthread_cond_x 主要API介绍

(1).静态初始化条件变量,与静态初始化互斥锁类似

static pthread_cond_t cond_name = PTHREAD_COND_INITIALIZER;

【注意】:与mutex相同,SUSv3规定:将条件变量一系列的操作pthread_cond_x应用于条件变量的副本(copy)时会导致未定义的行为,所有操作仅能针对条件变量的原本执行

(2).pthread_cond_signal & pthread_cond_broadcast(通知)

【头文件】(编译时要加上库-lpthread)

#include <pthread.h>

【函数原型】

int pthread_cond_signal(pthread_cond_t *cond);

int pthread_cond_broadcast(pthread_cond_t *cond);

【函数功能】

pthread_cond_signal restarts one of the threads that are waiting on the condition variable cond. If no threads are waiting on cond, nothing happens. If several threads are waiting on cond, exactly one is restarted, but it is not specified which.

pthread_cond_broadcast restarts all the threads that are waiting on the condition variable cond. Nothing happens if no threads are waiting on cond.

可以看出signal与broadcast的区别在于唤醒的线程个数不同,signal只唤醒一个线程,但不确定是哪个;broadcast会唤醒全部等待线程,有点类似于"惊群效应",这里说一下"Nothing happens if no threads are waiting on cond"的意思:<Linux_Unix系统编程手册(上)>P531:条件变量并不保存状态信息,只是传递应用程序状态信息的一种通讯机制,发送信号时,若无任何线程在等待该条件变量,这个信号也就会不了了之,线程如在此后等待该条件变量,只有当再次收到此变量的下一信号时,方可解除阻塞状态

【返回值】

RETURN VALUE
       All condition variable functions return 0 on success and a non-zero error code on error.

ERRORS
       pthread_cond_init, pthread_cond_signal, pthread_cond_broadcast, and pthread_cond_wait never return an error code.

(3).pthread_cond_wait & pthread_cond_timedwait(等待)

【头文件】#include <pthread.h>

【函数原型】

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);

【函数功能】

pthread_cond_wait atomically unlocks the mutex (as per pthread_unlock_mutex) and waits for the condition variable cond to be signaled. The thread  execution is  suspended  and  does  not  consume  any CPU time until the condition variable is signaled. The mutex must be locked by the calling thread on entrance to pthread_cond_wait. Before returning to the calling thread, pthread_cond_wait re-acquires mutex (as per pthread_lock_mutex).

Unlocking the mutex and suspending on the condition variable is done atomically. Thus, if all threads always acquire the mutex before signaling  the  condition, this guarantees that the condition cannot be signaled (and thus ignored) between the time a thread locks the mutex and the time it waits on the condition variable.

pthread_cond_timedwait atomically unlocks mutex and waits on cond, as pthread_cond_wait does, but it also bounds the duration of the wait. If cond  has  not been  signaled  within  the amount of time specified by abstime, the mutex mutex is re-acquired and pthread_cond_timedwait returns the error ETIMEDOUT.  The abstime parameter specifies an absolute time, with the same origin as time(2) and gettimeofday(2): an abstime of 0 corresponds to 00:00:00 GMT,  January  1, 1970.

需要对pthread_cond_wait的行为加以阐述说明:(<Linux_Unix系统编程手册(上)>P532),条件变量总是要与一个互斥量相关,将这些对象通过函数参数传递给pthread_cond_wait,后者执行如下操作:

*解锁互斥量mutex

*堵塞调用线程,直至另一线程就条件变量cond发出信号

*重新锁定mutex

互斥量的释放与陷入对条件变量的等待同属于一个原子操作,换句话说,在函数pthread_cond_wait的调用线程陷入对条件变量的等待之前,其他线程不可能获取到该互斥量,也就不可能就该条件变量发出信号(如果解锁与阻塞不是一个原子操作的话,其余线程很有可能在解锁后阻塞前获得互斥锁改变条件发出通知,那么这个通知可能因为没有等待线程的存在而无效)

3.条件变量的使用-通用设计原则

通知者                                                                                 等待者

pthread_mutex_lock(&mutex);                                        pthread_mutex_lock(&mutex);                

/*change the state of shared variable*/                          while(/*checked that shared variable is not in the state we want*/)

pthread_mutex_unlock(&mutex);                                            pthread_cond_wait(&cond,&mutex);

                                                                                             /*now shared variable is in desired state,do some work here*/

                                                                                             pthread_mutex_unlock(&mutex);

4.条件变量的使用实例:线程B阻塞到线程A通知

root@ubuntu:/lianxi/lianxi_oj/thread_test# gcc pthread_cond_test.c -lpthread
root@ubuntu:/lianxi/lianxi_oj/thread_test# ./a.out
[main]pthread_create success!
[thrd]in hal_thread...
[thrd]thread lock...
[main]main lock...
[main]waiting thread end...
[thrd]cond_wait ret_val = 0
[thrd]thread modifying...
[main]g_var = 1000
root@ubuntu:/lianxi/lianxi_oj/thread_test# 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <pthread.h>

/************************MACROS*************************/
#define TEST_SUCCESS (0)
#define TEST_FAILURE (-1)
#define TEST_UNMODIFIED (0)
#define TEST_MODIFIED (1)

#define TEST_LOCK() \
do { \
    pthread_mutex_lock(&g_mutex); \
} while(0)

#define TEST_UNLOCK() \
do { \
    pthread_mutex_unlock(&g_mutex); \
} while(0)


/********************GLOABAL_VARS**********************/
static pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t g_cond = PTHREAD_COND_INITIALIZER;
static pthread_t g_thread;
static int g_var = TEST_UNMODIFIED;

/*************************FUNCS************************/
void* hal_thread(void* args)
{
    int ret_val = TEST_FAILURE;
    printf("[thrd]in hal_thread...\n");
    TEST_LOCK();
        printf("[thrd]thread lock...\n");
        while(g_var == TEST_UNMODIFIED)
        {
            ret_val = pthread_cond_wait(&g_cond, &g_mutex);
            printf("[thrd]cond_wait ret_val = %d\n", ret_val);
        }
        printf("[thrd]thread modifying...\n");
        g_var = 1000;//we modified a large value here
    TEST_UNLOCK();
    return NULL;
}

int test_init(void)
{
    int ret_val = TEST_FAILURE;
    ret_val = pthread_create(&g_thread, NULL, hal_thread, NULL);
    if(ret_val != TEST_SUCCESS)
    {
        printf("[main]pthread_create failed!\n");
        return TEST_FAILURE;
    }
    printf("[main]pthread_create success!\n");
    return TEST_SUCCESS;
}

int test_inform(void)
{
    int ret_val = TEST_FAILURE;

    TEST_LOCK();
        printf("[main]main lock...\n");
        g_var = TEST_MODIFIED;
    TEST_UNLOCK();
    ret_val = pthread_cond_broadcast(&g_cond);
    if(ret_val != TEST_SUCCESS)
    {
        printf("[main]pthread_cond_broadcast error!\n");//pthread_cond_broadcast will never returns an error code
        return TEST_FAILURE;
    }
    return TEST_SUCCESS;
}

int main(int argc, char* argv[])
{
    int ret_val = TEST_FAILURE;
    
    ret_val = test_init();
    if(ret_val != TEST_SUCCESS)
    {
        printf("[main]test_init failed!\n");
        return TEST_FAILURE;
    }
    sleep(1);

    ret_val = test_inform();
    if(ret_val != TEST_SUCCESS)
    {
        printf("[main]test_inform failed!\n");
        return TEST_FAILURE;
    }

    printf("[main]waiting thread end...\n");
    ret_val = pthread_join(g_thread, NULL);
    if(ret_val != TEST_SUCCESS)
    {
        printf("[main]pthread_join failed!\n");
        return TEST_FAILURE;
    }
    printf("[main]g_var = %d\n", g_var);
    return TEST_SUCCESS;
}

5.工作中遇到的条件变量的用法

最近在工作中也遇到了条件变量的用法,用了一个函数涵盖了几种不同的等待,而且对pthread_cond_timedwait的超时时间也有说明,可以在此记录一下,因为是伪代码,主要看思路和注释,可以看到一个函数中包含了三种不同的等待行为:无限等待,立即返回和超时等待

/*sample of pthread_cond_x*/
#define TEST_timeoutInfinity (0xFFFFFFFF)
#define TEST_timeoutImmediate (0)
/*pSemaphoreId只是一个malloc的地址, 用来找malloc出来的互斥量与条件变量的*/
int TEST_WaitSemaphoreTimeout(TEST_sid_t* pSemaphoreId, unsigned int relTimeout)
{
    /*Time structure needed to get time of day*/
    struct timeval now;

    /*Time structire to pass to pthread_cond_timedwait()*/
    struct timespec absTimeout;//absolute timeout

    TEST_internalSem_t* pSem = (TEST_internalSem_t*)(pSemaphoreId);//仅仅是为了找到malloc出来的互斥量与条件变量

    /*带nano的表示纳秒, sec表示秒, 因为now的精度是微秒, 而absTimeout的精度是纳秒*/
    unsigned int dnanos, dsecs, ananos, asecs, cnanos;

    /*timeout infinity*/
    if(relTimeout == TEST_timeoutInfinity)
    {
        pthread_mutex_lock(&pSem->mutex);
        while(pSem->count == 0)
        {
            pthread_cond_wait(&pSem->cond, &pSem->mutex);
        }
        --(pSem->count);
        pthread_mutex_unlock(&pSem->mutex);
        return TEST_SEMAPHORE_SUCCESS;
    }

    /*timeout immediate*/
    if(relTimeout == TEST_timeoutImmediate)
    {
        pthread_mutex_lock(&pSem->mutex);
        if(pSem->count == 0)
        {
            pthread_mutex_unlock(&pSem->mutex);
            return TEST_SEMAPHORE_TIMEOUT;
        }
        --(pSem->count);
        pthread_mutex_unlock(&pSem->mutex);
        return TEST_SEMAPHORE_SUCCESS;
    }

    /*timeout sometime*/
    gettimeofday(&now, NULL);

    /*time transfer here*/
    time_transfer();

    pthread_mutex_lock(&pSem->mutex);
    while(pSem->count == 0)
    {
        if(pthread_cond_timedwait(&pSem->cond, &pSem->mutex, &absTimeout) != 0)
        {
            pthread_mutex_unlock(&pSem->mutex);
            return TEST_SEMAPHORE_TIMEOUT;
        }
    }
    --(pSem->count);
    pthread_mutex_unlock(&pSem->mutex);
    return TEST_SEMAPHORE_SUCCESS;
}

6.pthread_cond_timedwait中超时时间的计算方法

首先复习一下pthread_cond_timedwait的MAN手册介绍:

pthread_cond_timedwait atomically unlocks mutex and waits on cond, as pthread_cond_wait does, but it also bounds the duration of the wait. If cond  has  not been  signaled  within  the amount of time specified by abstime, the mutex mutex is re-acquired and pthread_cond_timedwait returns the error ETIMEDOUT.  The abstime parameter specifies an absolute time, with the same origin as time(2) and gettimeofday(2): an abstime of 0 corresponds to 00:00:00 GMT,  January  1, 1970.

我们知道超时等待最后一个参数是const struct timespec *abstime,结合MAN手册的描述,这是一个absolute time,即并不是随便指定一个超时时间就完了,而是绝对时间(当前时间+超时时间)

再来看两个结构体

struct timeval {
    time_t      tv_sec;     /* seconds */
    suseconds_t tv_usec;    /* microseconds */
};

struct timespec {
    time_t tv_sec; /*seconds */
    long tv_nsec; /*nanoseconds */
};

相关时间单位的换算如下:

1秒(second) == 1000毫秒(millisecond)

1毫秒(millisecond) == 1000微秒(microsecond)

1微秒(microsecond) == 1000纳秒(nanosecond)

所以函数gettimeofday得到的是当前时间的微秒精度,而超时等待函数需要一个(当前时间+超时时间)的纳秒精度,就存在一个换算的问题,算法的伪代码如下,算法的核心就是总的秒数=now_sec+timeout_sec,总的纳秒数=now_nano+timeout_nano,只是需要考虑当两个纳秒数之和大于1s的情况,在这里贴上一篇博客,博客中的算法与我贴出的思路一致:

https://blog.csdn.net/wang_xijue/article/details/31758843

time_transfer(unsigned int relTimeout)
{
    /*We convert the timeval structure into a timespec one.
     *In the same time we add the relative time to wait
     *relTimeout is in millisecond, so relTimeout/1000 is the number of seconds in relTimeout
     *and relTimeout%1000 is the remainder of the division
     *it is to say:the number of milliseconds remainding*/
     
    /*Time structure needed to get time of day*/
    struct timeval now;

    /*Time structire to pass to pthread_cond_timedwait()*/
    struct timespec absTimeout;//absolute timeout

    /*带nano的表示纳秒, sec表示秒, 因为now的精度是微秒, 而absTimeout的精度是纳秒*/
    unsigned int dnanos, dsecs, ananos, asecs, cnanos;

    gettimeofday(&now, NULL);

    if(relTimeout < 3600000)//3600000毫秒==3600秒==60分钟
    {
        /*relTimeout*/
        dsecs = relTimeout/1000;//seconds part of the timeout(relTimeout整秒的部分, 假如是1500毫秒, 刚好是1个1000毫秒+500毫秒)
        dnanos = (relTimeout-dsecs*1000)*1000*1000;//nano part of the timeout(相当于(relTimeout%1000)*1000*1000)

        /*current time*/
        cnanos = now.tv_usec*1000;//current nanos(将gettimeofday得到的当前时间的微秒换算成秒)

        /*time transfer:now_sec+timeout_sec, now_nano+timeout_nano*/
        /*因为cnanos < 1s而且dnanos < 1s, 由不等式的性质知道cnanos+dnanos<2s, 需要考虑两个nano相加大于1s的情况*/
        if((cnanos+dnanos) >= 1*1000*1000*1000)
        {
            asecs = now.tv_sec+dsecs+1;
            ananos = cnanos+dnanos-1*1000*1000*1000;
        }
        else
        {
            asecs = now.tv_sec+dsecs;
            ananos = cnanos+dnanos;
        }
        absTimeout.tv_sec = asecs;
        absTimeout.tv_nsec = ananos;
    }
    else//do not know why here
    {
        absTimeout.tv_sec = now.tv_sec+(relTimeout/1000)+1;
        absTimeout.tv_nsec = 0;
    }
}

7.pthread_cond_signal/broadcast是放在锁内还是锁外呢

Linux下推荐放在锁内

pthread_mutex_lock

xxxxxxx

pthread_cond_signal/broadcast

pthread_mutex_unlock

具体原因请见博客:

https://blog.csdn.net/yeyuangen/article/details/37593533

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值