在上一篇文章《深入理解Linux 条件变量2:使用条件变量实现[生产-消费]框架》中,我们通过示例代码演示了条件变量的使用。从条件变量的API接口中我们很容易发现,条件变量必须配合着互斥锁使用,那么问题来了:
条件变量的设计,为什么要结合着互斥锁使用呢?

针对这个问题,网上有一种流行说法:

条件变量配合着互斥量使用,是为了解决 在线程A中调用了 pthread_cond_wait,但是线程A还没有进入wait cond的时候,线程B调用了pthread_cond_signal,如果不用mutex,这个cond_signal就丢失了,而如果加上了锁,则线程B必须要等到mutex被释放了的时候才能调用pthread_cond_signal。

这种说法貌似是挺有道理的,我们会想当然的以为条件变量就是个普通变量,或者说是个单独的,类似于int var这种的独立变量,信号发送,如果没有cond_wait,这个信号就丢失了。但事实是这样的吗?

实践是检验真理的唯一条件,在上一篇文章《深入理解Linux 条件变量2:使用条件变量实现[生产-消费]框架》 代码中,在main中,我们先创建了生产线程,然后等待了2s才创建消费线程,代码如下:

  	pthread_create(&pid, NULL, th_producer, NULL);
  	sleep(2);
 	printf("start create consumer thread.\n");
    pthread_create(&cid, NULL, th_consumer, (void *)th_consum1);

而在生产线程中每隔100ms就向消息队列中添加一个节点消息,然后调用pthread_cond_signal 向条件变量发送信号,如果按照上面的说法,理论上在消费线程运行之前的pthread_cond_signal都会丢失,那么程序运行结果呢? 如下:

start create consumer thread.
------- [th_consum1] get 0 --------
------- [th_consum1] get 1 --------
------- [th_consum1] get 2 --------
------- [th_consum1] get 3 --------
------- [th_consum1] get 4 --------
------- [th_consum1] get 5 --------
------- [th_consum1] get 6 --------
------- [th_consum1] get 7 --------
------- [th_consum1] get 8 --------
------- [th_consum1] get 9 --------
------- [th_consum1] get 10 --------
------- [th_consum1] get 11 --------
------- [th_consum1] get 12 --------
------- [th_consum1] get 13 --------

很明显,在pthread_cond_wait之前 的signal 并没有丢失,这就证明上面的说法是不对的,条件变量并不是一个简单的变量,而是一个计数器,每phread_cond_signal一次,则对条件变量计数递增,对应的,每pthread_cond_wait一次,则对条件变量计数递减,这样设计,我个人觉得对于开发者来说是非常友好的,开发者在实现自己的程序逻辑时,会更加方便和简单。

那既然不是上面说的原因,条件变量配合着锁的原因到底是什么呢?

条件变量配合互斥锁使用的真实原因

在《Unix环境高级编程》中,其实有答案,只不过答案太过简练了,没有过多的解释。我就以个人的理解来解释一下:

  1. 条件变量用于标识工作队列的状态。仅仅代表工作队列中有没有工作节点、工作队列状态变化,没有其他额外的含义,这本来就是它的设计初衷,只不过类似于linux链表结构 struct slist_s 设计一样,跟业务解耦了,开发者爱怎么用就怎么用,条件变量就是代表开发者自己的工作队列状态。
  2. 既然条件变量是标识工作队列的状态,那么我们在工作队列中的操作中使用了条件变量,这个条件变量就变成了工作队列的一部分,而工作队列一定是个共享资源,所以对于工作队列的操作,最好是要配合着互斥锁使用的,因为这样安全、有序。

我觉得答案就是这么简单,起码这种解释能够帮助你我更好的理解和使用条件变量。

pthread_cond_wait 对 mutex的影响

在上一篇文章《深入理解Linux 条件变量2:使用条件变量实现[生产-消费]框架》 代码中,消费者线程里,调用pthread_cond_wait前面 对mutex 加锁了:

 		pthread_mutex_lock(&mutex);

        while(slist_empty(&g_msg_head)){
            pthread_cond_wait(&cond_hasProduct, &mutex);
        }
        // 取消息队列的首个 节点消息
        slist_for_each_entry_safe(&g_msg_head, tmp, pos, msg_t, list){
            printf("------- [%s] get %d --------\n", name, pos->data);
            slist_del(&pos->list, &g_msg_head);
            free(pos);
            pos = NULL;
            break;
        }
        pthread_mutex_unlock(&mutex);

有些人可能会有疑问,pthread_cond_wait阻塞了线程,而前面已经调用了 pthread_mutex_lock,也就是对mutex加锁了,那别的线程是不是就再也拿不到mutex了?答案是:不会。
我们简单说一下调用pthread_cond_wait干了什么,简单的说做了下面2件事儿:

  1. 自动把调用线程放到等待条件的线程列表上。
  2. 对互斥量解锁。

注意: 上述2个动作,都是原子操作,所谓原子操作,就是这2个动作是连续的,并且不会被别的程序干扰,比如CPU调度,一定能保证这2个动作一气呵成。这就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道。

当条件变量有变化时,pthread_cond_wait 会返回,简单的说又做了下面2件事儿:

  1. 条件变量状态变化时,内核收到pthread_cond_signal信号,会从 等待条件的线程列表里,按照内部调度算法,唤醒至少1个线程。
  2. 被唤醒的线程中,pthread_cond_wait返回,并且互斥量会再次被锁住。

从上面的分析,也可以看出,条件变量的设计,既然包括了互斥量,内部也帮助开发者实现了对互斥锁的自动解锁和自动上锁。

Logo

更多推荐