条件变量的正确使用

条件变量的正确使用

陈硕 https://www.cnblogs.com/Solstice/p/3309089.html

TL;DR 如果你能一眼看出 https://gist.github.com/chenshuo/6430925 中的那 8 个 Waiter classes 哪些是对的哪些是错的,本文就不必看了。

我质疑某本书对 Pthreads 条件变量的封装是错的,因为它没有把 mutex 的 lock()/unlock() 函数暴露出来,导致无法实用。后来大家讨论的分歧是这个 cond class 是不是通用的条件变量封装,还是只是一个特殊的“事件等待器”。作为事件等待器,其实现也是错的,因为存在丢失事件的可能,可以算是初学者使用条件变量的典型错误。

本文的代码位于 recipes/thread/test/Waiter_test.cc,这里提到的某书的版本相当于 Waiter1 class。

我在拙作《Linux 多线程服务端编程:使用 muduo C++ 网络库》第 2.2 节总结了条件变量的使用要点:

条件变量只有一种正确使用的方式,几乎不可能用错。对于 wait 端:

  1. 必须与 mutex 一起使用,该布尔表达式的读写需受此 mutex 保护。
  2. 在 mutex 已上锁的时候才能调用 wait()。
  3. 把判断布尔条件和 wait() 放到 while 循环中。

对于 signal/broadcast 端:

  1. 不一定要在 mutex 已上锁的情况下调用 signal (理论上)。
  2. 在 signal 之前一般要修改布尔表达式。
  3. 修改布尔表达式通常要用 mutex 保护(至少用作 full memory barrier)。
  4. 注意区分 signal 与 broadcast:“broadcast 通常用于表明状态变化,signal 通常用于表示资源可用。(broadcast should generally be used to indicate state change rather than resource availability。)”

如果用条件变量来实现一个“事件等待器/Waiter”,正确的做法是怎样的?我的最终答案见 WaiterInMuduo class。“事件等待器”的一种用途是程序启动时等待初始化完成,也可以直接用 muduo::CountDownLatch 到达相同的目的,将初值设为 1 即可。

以下根据微博上的讨论过程给出几个正确或错误的版本,博大家一笑。只要记住 Pthread 的条件变量是边沿触发(edge trigger),即 signal()/broadcast() 只会唤醒已经等在 wait() 上的线程(s),我们在编码时必须要考虑 signal() 早于 wait() 的可能,那么就很容易判断以下各个版本的正误了。代码见 recipes/thread/test/Waiter_test.cc

此处贴上。

#include "../Condition.h"
#include "../Mutex.h"
#include "../Thread.h"

#include <boost/bind.hpp>

#include <signal.h>

// Interface of Waiter, also takes care of mutex_ & cond_ init and destroy.
class Waiter : boost::noncopyable
{
 public:
  virtual void wait() = 0;
  virtual void signal() = 0;

 protected:
  Waiter()
  {
    pthread_mutex_init(&mutex_, NULL);
    pthread_cond_init(&cond_, NULL);
  }

  ~Waiter()
  {
    pthread_mutex_destroy(&mutex_);
    pthread_cond_destroy(&cond_);
  }

  pthread_mutex_t mutex_;
  pthread_cond_t cond_;
};

// Version 1: orininal from the book
// Incorrect, could lose signal
class Waiter1 : public Waiter
{
 public:
  void wait() override
  {
    pthread_mutex_lock(&mutex_);
    pthread_cond_wait(&cond_, &mutex_);
    pthread_mutex_unlock(&mutex_);
  }

  void signal() override
  {
    pthread_cond_signal(&cond_);
  }
};

// Version 2: signal in lock
// Incorrect, could lose signal
class Waiter2 : public Waiter
{
 public:
  void wait() override
  {
    pthread_mutex_lock(&mutex_);
    pthread_cond_wait(&cond_, &mutex_);
    pthread_mutex_unlock(&mutex_);
  }

  void signal() override
  {
    pthread_mutex_lock(&mutex_);
    pthread_cond_signal(&cond_);
    pthread_mutex_unlock(&mutex_);
  }
};

// Version 3: add a boolean member
// Incorrect, spurious wakeup
class Waiter3 : public Waiter
{
 public:
  void wait() override
  {
    pthread_mutex_lock(&mutex_);
    if (!signaled_)
    {
      pthread_cond_wait(&cond_, &mutex_);
    }
    pthread_mutex_unlock(&mutex_);
  }

  void signal() override
  {
    pthread_mutex_lock(&mutex_);
    signaled_ = true;
    pthread_cond_signal(&cond_);
    pthread_mutex_unlock(&mutex_);
  }

 private:
  bool signaled_ = false;
};

// Version 4: wait in while-loop
// Correct, signal before unlock
class Waiter4 : public Waiter
{
 public:
  void wait() override
  {
    pthread_mutex_lock(&mutex_);
    while (!signaled_)
    {
      pthread_cond_wait(&cond_, &mutex_);
    }
    pthread_mutex_unlock(&mutex_);
  }

  void signal() override
  {
    pthread_mutex_lock(&mutex_);
    signaled_ = true;
    pthread_cond_signal(&cond_);
    pthread_mutex_unlock(&mutex_);
  }

 private:
  bool signaled_ = false;
};

// Version 5: wait in while-loop
// Correct, signal after unlock
class Waiter5 : public Waiter
{
 public:
  void wait() override
  {
    pthread_mutex_lock(&mutex_);
    while (!signaled_)
    {
      pthread_cond_wait(&cond_, &mutex_);
    }
    pthread_mutex_unlock(&mutex_);
  }

  void signal() override
  {
    pthread_mutex_lock(&mutex_);
    signaled_ = true;
    pthread_mutex_unlock(&mutex_);
    pthread_cond_signal(&cond_);
  }

 private:
  bool signaled_ = false;
};

// Note: version 4 is as efficient as version 5 because of "wait morphing"

// Version 6: signal before set boolean flag
// Correct or not?
class Waiter6 : public Waiter
{
 public:
  void wait() override
  {
    pthread_mutex_lock(&mutex_);
    while (!signaled_)
    {
      pthread_cond_wait(&cond_, &mutex_);
    }
    pthread_mutex_unlock(&mutex_);
  }

  void signal() override
  {
    pthread_mutex_lock(&mutex_);
    pthread_cond_signal(&cond_);
    signaled_ = true;
    pthread_mutex_unlock(&mutex_);
  }

 private:
  bool signaled_ = false;
};

// Version 8: modify signaled_ without lock
// Incorrect, data-race and could lose signal
class Waiter8 : public Waiter
{
 public:
  void wait() override
  {
    pthread_mutex_lock(&mutex_);
    while (!signaled_)
    {
      pthread_cond_wait(&cond_, &mutex_);
    }
    pthread_mutex_unlock(&mutex_);
  }

  void signal() override
  {
    signaled_ = true;
    pthread_cond_signal(&cond_);
  }

 private:
  bool signaled_ = false;
};

// Version 7: broadcast to wakeup multiple waiting threads
// Probably the best version among above.
class Waiter7 : public Waiter
{
 public:
  void wait() override
  {
    pthread_mutex_lock(&mutex_);
    while (!signaled_)
    {
      pthread_cond_wait(&cond_, &mutex_);
    }
    pthread_mutex_unlock(&mutex_);
  }

  void signal() override // Sorry, bad name in base class, poor OOP
  {
    broadcast();
  }

  void broadcast()
  {
    pthread_mutex_lock(&mutex_);
    pthread_cond_broadcast(&cond_);
    signaled_ = true;
    pthread_mutex_unlock(&mutex_);
  }

 private:
  bool signaled_ = false;
};

// Same as version 7, with muduo library
class WaiterInMuduo : boost::noncopyable
{
 public:
  WaiterInMuduo()
    : cond_(mutex_)
  {
  }

  void wait()
  {
    muduo::MutexLockGuard lock(mutex_);
    while (!signaled_)
    {
      cond_.wait();
    }
  }

  void broadcast()
  {
    muduo::MutexLockGuard lock(mutex_);
    signaled_ = true;
    cond_.notifyAll();
  }

 private:
  muduo::MutexLock mutex_;
  muduo::Condition cond_;
  bool signaled_ = false;
};

void initAndRefresh(Waiter* waiter)
{
  printf("init: running\n");

  struct timespec ts = { 0, 500*1000*1000 };
  nanosleep(&ts, NULL);  // initialization takes 500ms

  waiter->signal();
  printf("init: signaled\n");
}

void runServer(Waiter* waiter, int sec)
{
  muduo::Thread initThread(boost::bind(initAndRefresh, waiter));
  initThread.start();
  printf("main: init thread started\n");

  struct timespec ts = { sec, 0 };
  nanosleep(&ts, NULL); // do some work before calling wait()

  printf("main: waiting\n");
  waiter->wait();
  printf("main: done\n");
}

void sigalarm(int)
{
  write(1, "\nFAILED\n", 8);
  exit(1);
}

int main(int argc, char* argv[])
{
  signal(SIGALRM, sigalarm);
  alarm(5);

  Waiter1 w1;
  Waiter2 w2;
  Waiter3 w3;
  Waiter4 w4;
  Waiter5 w5;
  Waiter6 w6;
  Waiter7 w7;
  Waiter8 w8;
  WaiterInMuduo waiterInMuduo;

  Waiter* waiters[] = { NULL, &w1, &w2, &w3, &w4, &w5, &w6, &w7, &w8 };
  int whichWaiter = argc > 1 ? atoi(argv[1]) : 5;
  if (!(whichWaiter > 0 && whichWaiter <= 8))
  {
    printf("Unknown waiter, must between 1 and 8, inclusive.\n");
    return 1;
  }
  Waiter* waiter = waiters[whichWaiter];

  int sec = 0;
  printf("test 1: wait() before signal().\n");
  runServer(waiter, sec);

  printf("\ntest 2: wait() after signal().\n");
  sec = 1;
  runServer(waiter, sec);

  printf("\nPASSED!\n");
}

版本一:错误。某书上的原始版,有丢失事件的可能。

版本二:错误。lock() 之后再 signal(),同样有丢失事件的可能。

版本三:错误。引入了 bool signaled_; 条件,但没有正确处理 spurious wakeup。

版本四五六:正确。仅限 single waiter 使用。

版本七:最佳。可供 multiple waiters 使用。

版本八:错误。存在 data race,且有丢失事件的可能。理由见 http://stackoverflow.com/questions/4544234/calling-pthread-cond-signal-without-locking-mutex

总结:使用条件变量,调用 signal() 的时候无法知道是否已经有线程等待在 wait() 上。因此一般总是要先修改“条件”,使其为 true,再调用 signal();这样 wait 线程先检查“条件”,只有当条件不成立时才去 wait(),避免了丢事件的可能。换言之,通过使用“条件”,将边沿触发(edge trigger)改为电平触发(level trigger)。这里“修改条件”和“检查条件”都必须在 mutex 保护下进行,而且这个 mutex 必须用于配合 wait()。

思考题:如果用两个 mutex,一个用于保护“条件”,另一个专门用于和 cond 配合 wait(),会出现什么情况?

最后注明一点,http://stackoverflow.com/questions/6419117/signal-and-unlock-order 这篇帖子里对 spurious wakeup 的解释是错的,spurious wakeup 指的是一次 signal() 调用唤醒两个或以上 wait()ing 的线程,或者没有调用 signal() 却有线程从 wait() 返回。manpage 里对 Pthreads 系列函数的介绍非常到位,值得细读。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
条件变量是一种同步机制,用于线程之间的通信和协调。在 Python 中,条件变量可以用 threading 模块的 Condition 类来实现。 Condition 类有两个主要方法:wait() 和 notify()。wait() 方法用于等待条件变量的发生,如果条件不满足,线程就会阻塞在这里等待。notify() 方法用于通知等待条件变量的线程,让它们继续执行。 下面是一个简单的例子,演示了如何使用条件变量: ```python import threading class SharedCounter: def __init__(self, value=0): self.value = value self.cv = threading.Condition() def increment(self): with self.cv: self.value += 1 self.cv.notify_all() def decrement(self): with self.cv: while self.value == 0: self.cv.wait() self.value -= 1 counter = SharedCounter() def worker(): print('Starting worker') for i in range(10): counter.increment() print('Incremented: {}'.format(counter.value)) print('Exiting worker') threads = [] for i in range(5): t = threading.Thread(target=worker) t.start() threads.append(t) for t in threads: t.join() print('Final value: {}'.format(counter.value)) ``` 在这个例子中,我们创建了一个 SharedCounter 类,它包含一个整数值和一个条件变量。increment() 和 decrement() 方法分别用于增加和减少计数器的值。当计数器的值为 0 时,decrement() 方法会阻塞等待条件变量的发生。当 increment() 方法执行完成后,它会通知所有等待条件变量的线程,让它们继续执行。 我们创建了 5 个线程来同时访问 SharedCounter 对象,并在每个线程中调用 increment() 方法 10 次。由于 increment() 方法使用条件变量,线程之间可以协调和同步,保证计数器的值始终正确。最后,我们输出了计数器的最终值。 总的来说,条件变量是一种非常有用的同步机制,可以用于解决多线程程序中的一些复杂问题。在 Python 中,使用 threading 模块的 Condition 类来实现条件变量非常简单。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值