来自
https://www.zhihu.com/question/24116967?q=linux%20%E5%A4%9A%E7%BA%BF%E7%A8%8B%20%E8%99%9A%E5%81%87%E5%94%A4%E9%86%92%20%E9%97%AE%E9%A2%98%3F%E6%88%91%E6%80%8E%E4%B9%88%E8%A7%A3%E9%87%8A%E4%B8%8D%E5%90%8C%E8%82%AF%E5%AE%9A%E5%93%AA%E9%87%8C%E4%B8%8D%E5%AF%B9%E4%BA%86
吴志强
,还是喜欢在晚上写代码
——————更新,勘误——————
在原来的答案中,有这样的代码:
事实上,上面三行代码的并不是pthread_cond_wait(cv, mtx)的内联展开。其中第一行和第二行必须“原子化”,而第三行是可以分离出去的(之所以要把第三行放在里面的原因可以参见原来的答案)。
那么为什么第一行和第二行不能分离呢?这是因为必须得保证: 如果线程A先进入wait函数(即使没有进入实际的等待状态,比如正在释放mtx),那么必须得保证其他线程在其之后调用的broadcast必须能够将线程A唤醒。
所以,把原来答案中的代码再贴一遍:
如果执行序列是:a1, a2, a3, b1, b2, b3, b4, a4,那么线程A将不会被唤醒。而a3在线程B之前执行,这意味着wait函数是在signal之前调用的,所以不满足上文提到的保证。
解决办法:
因此,下面的代码是绝对不会出错的:
如果线程A先运行,那么执行序列必然是:a1, a2, a3, b1, b2, b3, b4, a4。
如果线程B先运行,那么执行序列可能是:b1, b2, b3, b4, a1, a2, a4
也可能是:b1, b2, b3, a1, a2, a3, b4, a4
所以,如果是我设计pthread API,那么我会添加一个pthread_cond_unlock_and_wait函数,伪代码如下:
这样的好处在于:如果我们可以保证没有虚假唤醒(即不需要while循环测试条件),那么我们可以将线程A的代码改成上述形式,这样无论怎样都只需要执行一次pthread_mutex_unlock()函数,而之前的版本至少需要执行两次。
—————— 原来的答案——————
感谢大家的回答!
看了之后,我获得了启发,突然觉得这或许是跟条件变量的通常用法有关。
首先需要明白两点:
然后,我们假设wait()操作不会自动释放、获取锁,那么代码会变成这样:
久而久之,程序员发现unlock, just_wait, lock这三个操作始终得在一起。于是就提供了一个pthread_cond_wait()函数来同时完成这三个函数。
另外一个证据是,signal()函数是不需要传递mutex参数的,所以关于mutex参数是用于同步wait()和signal()函数的说法更加站不住脚。
所以我的结论是: 传递的mutex并不是为了防止wait()函数内部的Race Condition!而是因为调用wait()之前你总是获得了某个mutex(例如用于解决此处pass变量的Race Condition的mutex),并且这个mutex在你调用wait()之前必须得释放掉,调用wait()之后必须得重新获取。
所以,pthread_cond_wait()函数不是一个细粒度的函数,却是一个实用的函数。
在原来的答案中,有这样的代码:
pthread_mutex_unlock(mtx); pthread_cond_just_wait(cv); pthread_mutex_lock(mtx);
那么为什么第一行和第二行不能分离呢?这是因为必须得保证: 如果线程A先进入wait函数(即使没有进入实际的等待状态,比如正在释放mtx),那么必须得保证其他线程在其之后调用的broadcast必须能够将线程A唤醒。
所以,把原来答案中的代码再贴一遍:
// 线程A,条件测试 pthread_mutex_lock(mtx); // a1 while(pass == 0) { // a2 pthread_mutex_unlock(mtx); // a3 pthread_cond_just_wait(cv); // a4 pthread_mutex_lock(mtx); // a5 } pthread_mutex_unlock(mtx); // 线程B,条件发生修改,对应的signal代码 pthread_mutex_lock(mtx); // b1 pass = 1; // b2 pthread_mutex_unlock(mtx); // b3 pthread_cond_signal(cv); // b4
如果执行序列是:a1, a2, a3, b1, b2, b3, b4, a4,那么线程A将不会被唤醒。而a3在线程B之前执行,这意味着wait函数是在signal之前调用的,所以不满足上文提到的保证。
解决办法:
- 先将线程附加到等待队列
- 释放mutex
- 进入等待
因此,下面的代码是绝对不会出错的:
// 线程A,条件测试
pthread_mutex_lock(mtx); // a1
while(pass == 0) { // a2
pthread_cond_wait(cv, mtx); // a3
}
pthread_mutex_unlock(mtx); // a4
// 线程B,条件发生修改,对应的signal代码
pthread_mutex_lock(mtx); // b1
pass = 1; // b2
pthread_mutex_unlock(mtx); // b3
pthread_cond_signal(cv); // b4
如果线程B先运行,那么执行序列可能是:b1, b2, b3, b4, a1, a2, a4
也可能是:b1, b2, b3, a1, a2, a3, b4, a4
所以,如果是我设计pthread API,那么我会添加一个pthread_cond_unlock_and_wait函数,伪代码如下:
int pthread_cond_wait(cv, mtx) { int ret = pthread_cond_unlock_and_wait(cv, mtx); pthread_mutex_lock(mtx); return ret; } // 线程A,条件测试 pthread_mutex_lock(mtx); if (pass == 0) pthread_cond_unlock_and_wait(cv, mtx); else pthread_mutex_unlock(mtx); // 线程B,条件发生修改,对应的signal代码 pthread_mutex_lock(mtx); // b1 pass = 1; // b2 pthread_mutex_unlock(mtx); // b3 pthread_cond_signal(cv); // b4
—————— 原来的答案——————
感谢大家的回答!
看了之后,我获得了启发,突然觉得这或许是跟条件变量的通常用法有关。
首先需要明白两点:
- wait()操作通常伴随着条件检测,如:
while(pass == 0) pthread_cond_wait(...);
- signal*()函数通常伴随着条件改变,如:
pass = 1; pthread_cond_signal(...)
// 条件测试 pthread_mutex_lock(mtx); while(pass == 0) pthread_cond_wait(...); pthread_mutex_unlock(mtx); // 条件发生修改,对应的signal代码 pthread_mutex_lock(mtx); pass = 1; pthread_mutex_unlock(mtx); pthread_cond_signal(...);
然后,我们假设wait()操作不会自动释放、获取锁,那么代码会变成这样:
// 条件测试 pthread_mutex_lock(mtx); while(pass == 0) { pthread_mutex_unlock(mtx); pthread_cond_just_wait(cv); pthread_mutex_lock(mtx); } pthread_mutex_unlock(mtx); // 条件发生修改,对应的signal代码 pthread_mutex_lock(mtx); pass = 1; pthread_mutex_unlock(mtx); pthread_cond_signal(cv);
久而久之,程序员发现unlock, just_wait, lock这三个操作始终得在一起。于是就提供了一个pthread_cond_wait()函数来同时完成这三个函数。
另外一个证据是,signal()函数是不需要传递mutex参数的,所以关于mutex参数是用于同步wait()和signal()函数的说法更加站不住脚。
所以我的结论是: 传递的mutex并不是为了防止wait()函数内部的Race Condition!而是因为调用wait()之前你总是获得了某个mutex(例如用于解决此处pass变量的Race Condition的mutex),并且这个mutex在你调用wait()之前必须得释放掉,调用wait()之后必须得重新获取。
所以,pthread_cond_wait()函数不是一个细粒度的函数,却是一个实用的函数。