条件变量的陷阱与思考

一、前言

  在多线程编程中,互斥锁与条件变量恐怕是最常用也是最实用的线程同步原语。

  关于条件变量一共也就pthread_cond_init、pthread_cond_destroy、pthread_cond_wait、pthread_cond_timedwait、pthread_cond_signal、pthread_cond_broadcast这么几个函数,但是在实际使用中却是很容易用错,后文将来分析几种常见使用情况的正确性。

二、分析

  下面是一个辅助基类、便于减少篇幅(由于简单起见,后文中的所有函数调用并未检查返回的错误情况):

 1 class ConditionBase
 2 {
 3 public:
 4     ConditionBase()
 5     {
 6         pthread_mutex_init(&mutex_, NULL);
 7         pthread_cond_init(&cond_, NULL);
 8     }
 9
10     ~ConditionBase()
11     {
12         pthread_mutex_destroy(&mutex_);
13         pthread_cond_destroy(&cond_);
14     }
15
16 private:
17     pthread_mutex_t  mutex_;
18     pthread_cond_t   cond_;
19 };

  版本一:

 1 class Condition1 : public ConditionBase
 2 {
 3 public:
 4     void wait()
 5     {
 6         pthread_mutex_lock(&mutex_);
 7         pthread_cond_wait(&cond_, &mutex_);
 8         pthread_mutex_unlock(&mutex_);
 9     }
10
11     void wakeup()
12     {
13         pthread_cond_signal(&cond_);
14     }
15 };

  错误,这种情况有可能丢失事件。当signal发生在wait之前,就会丢失这次signal事件。如下图

  版本二:

 1 class Condition2 : public ConditionBase
 2 {
 3 public:
 4     void wait()
 5     {
 6         pthread_mutex_lock(&mutex_);
 7         pthread_cond_wait(&cond_, &mutex_);
 8         pthread_mutex_unlock(&mutex_);
 9     }
10
11     void wakeup()
12     {
13         pthread_mutex_lock(&mutex_);
14         pthread_cond_signal(&cond_);
15         pthread_mutex_unlock(&mutex_);
16     }
17 };

  错误,同情况一一样有可能丢失事件。当signal事件发生在wait之前就会丢失signal事件。如下图

  版本三:

 1 class Condition3 : public ConditionBase
 2 {
 3 public:
 4     Condition3()
 5         : signal_(false)
 6     {
 7     }
 8
 9     void wait()
10     {
11         pthread_mutex_lock(&mutex_);
12         if (!signal_)
13         {
14             pthread_cond_wait(&cond_, &mutex_);
15         }
16         pthread_mutex_unlock(&mutex_);
17     }
18
19     void wakeup()
20     {
21         pthread_mutex_lock(&mutex_);
22         signal_ = true;
23         pthread_cond_signal(&cond_);
24         pthread_mutex_unlock(&mutex_);
25     }
26
27 private:
28     bool signal_;
29 };

  错误。引入了bool变量来检查状态,但是遇到spurious wakeup仍然会发生错误。

  什么是spurious wakeup?Wikipedia中是这样说的:

Spurious wakeup describes a complication in the use of condition variables as provided by certain multithreading APIs such as POSIX Threads and the Windows API. Even after a condition variable appears to have been signaled from a waiting thread‘s point of view, the condition that was awaited may still be false. One of the reasons for this is a spurious wakeup; that is, a thread might be awoken from its waiting state even though no thread signaled the condition variable.

  也就是说一次signal调用唤醒了2个或者2个以上的waiting中的线程,这种现象就是spurious wakeup,虚假唤醒。

  APUE上这样说:POSIX规范为了简化实现,允许pthread_cond_signal在实现的时候可以唤醒不止一个线程。

  在发生的spurious wakeup时候,waiting线程被唤醒,然后到真正signal的时候,waiting线程在之前已经spurious wakeup唤醒了,这样就会造成不易debug的错误。如下图

  版本四:

 1 class Condition4 : public ConditionBase
 2 {
 3 public:
 4     Condition4()
 5         : signal_(false)
 6     {
 7     }
 8
 9     void wait()
10     {
11         pthread_mutex_lock(&mutex_);
12         while (!signal_)
13         {
14             pthread_cond_wait(&cond_, &mutex_);
15         }
16         pthread_mutex_unlock(&mutex_);
17     }
18
19     void wakeup()
20     {
21         pthread_mutex_lock(&mutex_);
22         signal_ = true;
23         pthread_cond_signal(&cond_);
24         pthread_mutex_unlock(&mutex_);
25     }
26
27 private:
28     bool signal_;
29 };

  正确。这个是推荐用法,APUE, UNP,man手册中都是这种用法,在wait上用while循环而不是if就可以正确处理spurious wakeup情况了。当发生spurious wakeup时,wait被意料之外的唤醒,但是循环条件并没有改变,于是循环继续执行pthread_cond_wait,然后继续进入wait,等待被正确的唤醒。

  版本五:

class Condition5 : public ConditionBase
{
public:
    Condition5()
        : signal_(false)
    {
    }

    void wait()
    {
        pthread_mutex_lock(&mutex_);
        while (!signal_)
        {
            pthread_cond_wait(&cond_, &mutex_);
        }
        pthread_mutex_unlock(&mutex_);
    }

    void wakeup()
    {
        pthread_mutex_lock(&mutex_);
        signal_ = true;
        pthread_mutex_unlock(&mutex_);
        pthread_cond_signal(&cond_);
    }

private:
    bool signal_;
};

  正确。版本五与版本四的唯一区别就是在唤醒的时候先解锁再调用signal发起唤醒。这么做不会有错误,但是可能有较大几率会使线程调度不在最理性状态,例如在wakeup调用中的解锁以后,调用signal以前,系统调度发生线程切换,使得signal没有在第一时间被发出。

  这是在stackoverflow的一个帖子中的说法:http://stackoverflow.com/questions/4544234/calling-pthread-cond-signal-without-locking-mutex

Note that you can actually move the pthread_cond_signal() itself after the pthread_mutex_unlock(), but this can result in less optimal scheduling of threads, and you‘ve necessarily locked the mutex already in this code path due to changing the condition itself.

  版本六:

 1 class Condition6 : public ConditionBase
 2 {
 3 public:
 4     Condition6()
 5         : signal_(false)
 6     {
 7     }
 8
 9     void wait()
10     {
11         pthread_mutex_lock(&mutex_);
12         while (!signal_)
13         {
14             pthread_cond_wait(&cond_, &mutex_);
15         }
16         pthread_mutex_unlock(&mutex_);
17     }
18
19     void wakeup()
20     {
21         pthread_mutex_lock(&mutex_);
22         pthread_cond_signal(&cond_);
23         signal_ = true;
24         pthread_mutex_unlock(&mutex_);
25     }
26
27 private:
28     bool signal_;
29 };

  正确,版本六与版本四的区别状态的改变和发起signal唤醒信号的顺序互换了,由于整个wakeup过程都在mutex的包含之下,所以并没有影响。但是个人更推荐版本四,因为更符合逻辑,不然APUE和UNP也不会都用版本四的写法顺序了:)

  版本七:

 1 class Condition7 : public ConditionBase
 2 {
 3 public:
 4     Condition7()
 5         : signal_(false)
 6     {
 7     }
 8
 9     void wait()
10     {
11         pthread_mutex_lock(&mutex_);
12         while (!signal_)
13         {
14             pthread_cond_wait(&cond_, &mutex_);
15         }
16         pthread_mutex_unlock(&mutex_);
17     }
18
19     void wakeup()
20     {
21         pthread_mutex_lock(&mutex_);
22         signal_ = true;
23         pthread_cond_broadcast(&cond_);
24         pthread_mutex_unlock(&mutex_);
25     }
26
27 private:
28     bool signal_;
29 };

  正确。与版本四的区别是这里用了broadcast,而不是signal。对于这种只有一个wait线程的时候,是没有问题的。但是当有多个wait线程的时候,使用broadcast就把所有wait线程都唤醒了。

  另外,当我们使用条件变量cond实现事件等待器的时候,就要用broadcast而不是signal了,因为当有多个事件挂起在wait调用上等待时,signal只能唤醒其中的一个等待线程,并且我们不能期待它唤醒具体的某一个线程,因为这个是不可控的。

  版本八:

 1 class Condition8 : public ConditionBase
 2 {
 3 public:
 4     Condition8()
 5         : signal_(false)
 6     {
 7     }
 8
 9     void wait()
10     {
11         pthread_mutex_lock(&mutex_);
12         while (!signal_)
13         {
14             pthread_cond_wait(&cond_, &mutex_);
15         }
16         pthread_mutex_unlock(&mutex_);
17     }
18
19     void wakeup()
20     {
21         signal_ = true;
22         pthread_cond_signal(&cond_);
23     }
24
25 private:
26     bool signal_;
27 };

  错误。存在data race,从而导致有可能丢失事件。当wakeup调用发生在wait调用中的进入while循环之后,调用pthread_cond_wait之前,就会丢失signal事件。如下图

  另外,在wait调用中,必须用一个mutex同时保护条件状态和cond的pthread_cond_wait的调用,而不能用2个mutex,一个保护条件状态,一个保护pthread_cond_wait,pthread_cond_signal的调用。

  这样仍然会出现race condition。比如先发生wait调用保护条件的mutex加锁、检查条件、解锁,然后切换线程调用wakeup发送signal信号,再切会wait线程,包含cond的mutex加锁、进入pthread_cond_wait的waiting状态中,从而丢失了之前的signal事件。

三、总结

  使用条件变量,调用signal/broadcast的时候,无法知道是否已经有线程等在wait上了。因此,一般要先改变条件状态,然后再发送signal/broadcast信号。然后在wait调用线程上先检查条件状态,只有当条件状态为假的时候才进入pthread_cond_wait进行等待,从而防止丢失signal/broadcast事件。并且检查条件、pthread_cond_wait,修改条件、signal/broadcast都要在同一个mutex的保护下进行。

时间: 2024-10-24 15:46:47

条件变量的陷阱与思考的相关文章

Linux线程条件变量成为取消点的陷阱

Linux线程条件变量成为取消点的陷阱 使用 pthread_cancel() 时,线程往往不会直接退出,而需要运行到取消点. pthread_cond_wait() 作为线程常见的一种阻塞,它也是一个取消点.所以,处于条件变量阻塞的线程在接收到取消信号就会直接退出. 然而,由于条件变量需要搭配互斥量使用,进入 pthread_cond_wait() 意味着互斥量上锁,此时退出线程如果不进行解锁,而且同时其他线程正在等待此条件变量,那么就会陷入死锁.因此建议使用cleanup函数在线程退出时对互

操作系统思考 第十章 条件变量

第十章 条件变量 作者:Allen B. Downey 原文:Chapter 10 Condition variables 译者:飞龙 协议:CC BY-NC-SA 4.0 像上一章所展示的那样,许多简单的同步问题都可以用互斥体解决.这一章中我会介绍一个更大的挑战,著名的"生产者-消费者"问题,以及一个用于解决它的新工具,条件变量. 10.1 工作队列 在一些多线程的程序中,线程被组织用于执行不同的任务.通常它们使用队列来相互通信,其中一些线程叫做"生产者",向队列

linux系统编程:线程同步-条件变量(cond)

线程同步-条件变量(cond) 生产者与消费者问题 再引入条件变量之前,我们先看下生产者和消费者问题:生产者不断地生产产品,同时消费者不断地在消费产品. 这个问题的同步在于两处:第一,消费者之间需要同步:同一件产品只可由一人消费.第二,当无产品可消费时,消费者需等待生产者生产后,才可继续消费,这又是一个同步问题.详细了解:生产者消费者问题. 条件变量 条件变量是利用线程间共享的全局变量进行同步的一种机制,并且条件变量总是和互斥锁结合在一起. 相关函数 pthread_cond_t //条件变量类

条件变量---生产者消费者问题

假设有一个生产者线程,一个消费者线程,生产一个,消费一个.我们来看看怎么实现. #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <assert.h> int buffer; int count = 0; void put(int value) { assert(count == 0); count = 1; buffer = value; } int get() { a

Java并发程序设计(16)并发锁之条件变量

1.1.1. 条件变量应用之等待通知 条件变量Condition提供了一种基于ReentrantLock的事件等待和通知的机制,并且可以监控任意指定的条件,在条件不满足时等待条件满足,其它线程在条件满足时可以通知等待条件的线程,从而唤醒等待中的线程. 下面的代码实现了两件工作分别由两个线程轮流不断执行的效果. package com.test.concurrence; import java.util.concurrent.locks.Condition; import java.util.co

线程同步(条件变量、信号量)以及死锁

死锁:指两个或两个以上进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待现象,若无外力作用,它们都将无法继续推进下去. 例:交叉死锁:线程1获得了锁1,线程2获得了锁2,此时线程1调用lock想获得锁2,需挂起等待线程2释放锁2,而线程2也想获得锁1,也需挂起等待线程1释放锁1,此时两个线程都挂起等待 产生死锁的四个必要条件: (1):互斥条件(一个资源每次只能被一个进程或线程使用) (2):请求与保持条件(一个进程或线程因请求资源而阻塞时,对已获得的资源不释放) (3):不剥夺条件(此

条件变量函数

#include <pthread.h> #include <stdio.h> #include <stdlib.h> pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;/*初始化互斥锁*/ pthread_cond_t cond = PTHREAD_COND_INITIALIZER;/*初始化条件变量*/ void *thread1(void *); void *thread2(void *); int i=1; in

C++11并行编程-条件变量(condition_variable)详细说明

<condition_variable >头文件主要包含有类和函数相关的条件变量. 包括相关类 std::condition_variable和 std::condition_variable_any,还有枚举类型std::cv_status.另外还包含函数 std::notify_all_at_thread_exit(),以下分别介绍一下以上几种类型. std::condition_variable 类介绍 std::condition_variable是条件变量,很多其它有关条件变量的定义

详解条件变量

一年多过去啦,一段时间没有posix多线程的东西,又忘记的差不多略,我打记性咋这么差,丝毫记不起来怎么用啦,还是不如烂笔头啊. 大家都知道条件变量需要配合mutex一起使用,往往是这样的:lock->signal->unlock,  而另一边呢是: lock->wait->unlock. 在调用pthread_cond_wait(cond,mutex)时的执行顺序是这样的:1. 首先获取外面的mutex, 然后当前wait push 到一个等待的queue里面,然后释放锁.但是你看