在并发编程中,条件变量用来控制线程之间的同步,总的来说条件变量相关的操作主要有两个——wait和notify,在写博客时我有这样一些疑问:
1.wait操作是如何实现的,它是如何让线程进入睡眠的
2.notiy操作是如何做到通知正在等待的线程的,并且如何做到notify单个线程和notify所有线程的,如果是notify所有线程,那么这些被唤醒的线程可以并行执行吗(多核情况下)
3.为什么在调用wait操作时,需要传入mutex互斥锁,而notify时却不用。
4.c++中的条件变量为什么要配合unique_lock<mutex>锁而不是std::mutex使用或者lock_guard<mutex>
本文从以下几个方面对条件变量进行深入的理解
1.C++中的condition_variable实现原理
2.linux pthread库中的pthread condition variable实现原理
3.从操作系统源码的角度理解条件变量是如何实现的
4.有哪些错误的条件变量实现方式
条件变量使用的例子
例1
参考:C++ std::condition_variable 条件变量类探索:解锁条件变量的底层原理_std 条件变量-CSDN博客
include<iostream>// std::coutinclude<thread>// std::threadinclude<mutex>// std::mutex, std::unique_lockinclude<condition_variable>// std::condition_variableusingnamespace std;mutex mtx; // 互斥量
condition_variable cv; // 条件变量bool ready = false; // 标志量voidprint_id(int id){unique_lock<mutex> lck(mtx); // 上锁while (!ready) {cv.wait(lck); // 线程等待直到被唤醒(释放锁 + 等待,唤醒,在函数返回之前重新上锁)}cout << "thread " << id << '\n';
}voidgo(){unique_lock<mutex> lck(mtx); // 上锁ready = true;cv.notify_all(); // 唤醒所有正在等待(挂起)的线程(在这里面要释放锁,为了在wait函数返回之前能成功的重新上锁)
}intmain(){thread threads[10];for (int i = 0; i<10; ++i) {threads[i] = thread(print_id, i);}cout << "10 threads ready to race...\n";go();for (auto& th : threads) {th.join();}return0;
}
从这个例子可以看出,在调用notify和wait时,都需要进行加锁操作,所以mutex互斥锁锁住的其实是ready这个变量。到这里第二个问题中的“被唤醒的线程能否并行执行”也就有答案了,答案是不能,因为每个线程是需要获得互斥锁unique_lock<mutex>的。
对于第四个问题,答案是出于安全考虑,主要是防止程序员忘记对std::mutex进行unlock操作,特别是在一些异常处理中。至于为什么不用lock_guard[std::mutex](std::mutex),这个在后面介绍wait底层实现时说明
C++中的condition_variable实现原理
参考:C++ std::condition_variable 条件变量类探索:解锁条件变量的底层原理_std 条件变量-CSDN博客
c++ 11引入了条件变量,在gcc中的实现位于libstdc++-v3/include/std/condition_variable路径下(为了方面阅读,本文对源码略有修改)
class condition_variable{__condvar _M_cond;public:typedef __gthread_cond_t *native_handle_type;condition_variable() noexcept;~condition_variable() noexcept;condition_variable(const condition_variable &) = delete;condition_variable &operator=(const condition_variable &) = delete;voidnotify_one() noexcept;voidnotify_all() noexcept;voidwait(unique_lock<mutex> &__lock) {_M_cond.wait(*__lock.mutex());}template <typename _Predicate>voidwait(unique_lock<mutex> &__lock, _Predicate __p){while (!__p())wait(__lock);}}
可以看到wait/notify操作实际上是调用__condvar的接口,可以看到wait操作中获取了互斥量,从而在底层调用互斥量的lock/unlock接口,而lock_guard属于RAII 类,在构造函数中加锁,在析构函数中解锁,除此之外无法对其管理的互斥量进行lock/unlock接口。这里解答了第四个问题,由于wait底层操作中需要对互斥量进行lock/unlock操作,lock_guard并不满足这样的需求。
__condvar实际上是对glibc中pthread condition variable的一层包装,如下:
class __condvar{public:...voidwait(mutex &__m){int __e __attribute__((__unused__)) = __gthread_cond_wait(&_M_cond, __m.native_handle());__glibcxx_assert(__e == 0);}voidwait_until(mutex &__m, timespec &__abs_time){__gthread_cond_timedwait(&_M_cond, __m.native_handle(), &__abs_time);}...voidnotify_one() noexcept{int __e __attribute__((__unused__)) = __gthread_cond_signal(&_M_cond);__glibcxx_assert(__e == 0);}voidnotify_all() noexcept{int __e __attribute__((__unused__)) = __gthread_cond_broadcast(&_M_cond);__glibcxx_assert(__e == 0);}protected:
#ifdef __GTHREAD_COND_INITpthread_cond_t _M_cond = __GTHREAD_COND_INIT;
#elsepthread_cond_t _M_cond;
#endif};
可以看到,wait/notify操作底层主要是调用glibc的pthread condition variable接口。
glibc中的pthread condition variable实现原理
在网上找了一篇博客,从源码角度分析了条件变量的实现原理,细节暂时看不懂,以后再看吧深入了解glibc的条件变量_牛客网
看了这篇博客可以解答第三个问题,对于wait操作,其底层实现是需要对互斥量进行lock/unlock操作的,因此需要传入unique_lock
TODO