在并发编程中,条件变量用来控制线程之间的同步,总的来说条件变量相关的操作主要有两个——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 条件变量-优快云博客
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 条件变量-优快云博客
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;
void
notify_one() noexcept;
void
notify_all() noexcept;
void
wait(unique_lock<mutex> &__lock) {
_M_cond.wait(*__lock.mutex());
}
template <typename _Predicate>
void
wait(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:
...
void
wait(mutex &__m)
{
int __e __attribute__((__unused__)) = __gthread_cond_wait(&_M_cond, __m.native_handle());
__glibcxx_assert(__e == 0);
}
void
wait_until(mutex &__m, timespec &__abs_time)
{
__gthread_cond_timedwait(&_M_cond, __m.native_handle(), &__abs_time);
}
...
void
notify_one() noexcept
{
int __e __attribute__((__unused__)) = __gthread_cond_signal(&_M_cond);
__glibcxx_assert(__e == 0);
}
void
notify_all() noexcept
{
int __e __attribute__((__unused__)) = __gthread_cond_broadcast(&_M_cond);
__glibcxx_assert(__e == 0);
}
protected:
#ifdef __GTHREAD_COND_INIT
pthread_cond_t _M_cond = __GTHREAD_COND_INIT;
#else
pthread_cond_t _M_cond;
#endif
};
可以看到,wait/notify操作底层主要是调用glibc的pthread condition variable接口。
glibc中的pthread condition variable实现原理
在网上找了一篇博客,从源码角度分析了条件变量的实现原理,细节暂时看不懂,以后再看吧深入了解glibc的条件变量_牛客网
看了这篇博客可以解答第三个问题,对于wait操作,其底层实现是需要对互斥量进行lock/unlock操作的,因此需要传入unique_lock
TODO