std::condition_variable 中可能存在竞争条件?
Possible race condition in std::condition_variable?
我研究了 std::condition_variable(lock,pred)
的 VC++ 实现,基本上,它看起来像这样:
template<class _Predicate>
void wait(unique_lock<mutex>& _Lck, _Predicate _Pred)
{ // wait for signal and test predicate
while (!_Pred())
wait(_Lck);
}
基本上,裸体 wait
调用 _Cnd_waitX
,调用 _Cnd_wait
,调用 do_wait
,调用 cond->_get_cv()->wait(cs);
(所有这些都在文件 cond.c).
cond->_get_cv()
returns Concurrency::details::stl_condition_variable_interface
.
如果我们转到文件 primitives.h
,我们会看到在 windows 7 及更高版本下,我们有 class stl_condition_variable_win7
,其中包含旧的好 win32 CONDITION_VARIABLE
,并且 wait
调用 __crtSleepConditionVariableSRW
。
做一些汇编调试,__crtSleepConditionVariableSRW
只是提取 SleepConditionVariableSRW
函数指针,然后调用它。
事情是这样的:据我所知,win32 CONDITION_VARIABLE
不是内核对象,而是用户模式对象。因此,如果某个线程通知了这个变量,但实际上没有线程在它上面休眠,那么你就失去了通知,线程将保持休眠状态,直到超时或其他线程通知它。一个小程序实际上可以证明这一点——如果你错过了通知点——你的线程将继续休眠,尽管其他线程通知了它。
我的问题是这样的:
一个线程等待条件变量和谓词 returns false。然后,发生上面解释的整个调用链。那时,另一个线程改变了环境,所以谓词将 return true and 通知条件变量。我们在原线程中传递了predicate,但是还是没有进入SleepConditionVariableSRW
——调用链很长
所以,虽然我们通知了条件变量并且条件变量上的谓词肯定会return为真(因为通知者这样做了),我们仍然阻塞条件变量,可能永远阻塞。
这是它的行为方式吗?这似乎是一个巨大的、丑陋的竞争条件等待发生。如果您通知条件变量并且它的谓词 return 为真 - 线程应该解除阻塞。但是,如果我们在检查谓词和进入睡眠之间处于中间状态——我们将永远被阻塞。 std::condition_variable::wait
不是原子函数。
标准是怎么说的?它真的是竞争条件吗?
你违反了合同,所以所有的赌注都被取消了。参见:http://en.cppreference.com/w/cpp/thread/condition_variable
TLDR:当您持有互斥锁时,谓词不可能被其他人更改。
你应该在持有互斥锁的同时更改谓词的基础变量并且你必须在调用std::condition_variable::wait
之前获取该互斥锁(两者都是因为wait
释放互斥量,因为这是约定)。
在您描述的场景中,更改发生在 之后 while (!_Pred())
发现谓词不成立但在 wait(_Lck)
有机会释放之前互斥体。这意味着您在不持有互斥体的情况下更改了谓词检查的内容。你违反了规则,竞争条件或无限等待仍然不是你能得到的最糟糕的 UB 类型。至少这些是本地的并且与您违反的规则相关,因此您可以找到错误...
如果您遵守规则,则:
- 服务员先取得互斥量
- 进入
std::condition_variable::wait
。 (回想一下,通知程序仍在等待互斥量。)
- 检查谓词并发现它不成立。 (回想一下通知程序 仍然 在互斥体上等待。)
- 调用一些实现定义的魔法来释放互斥锁并等待,只有现在通知程序才能继续。
- 通知程序终于成功获取了互斥量。
- 通知者更改任何需要更改以使谓词成立的内容。
- 通知程序调用
std::condition_variable::notify_one
。
或:
- 通知程序获取互斥量。 (回想一下,服务员在尝试获取互斥锁时被阻止。)
- 通知者更改任何需要更改以使谓词成立的内容。 (回想一下,服务员还是被堵住了。)
- 通知程序释放互斥量。 (在途中的某个地方,服务员会调用
std::condition_variable::notify_one
,但是一旦互斥体被释放...)
- 服务员获取互斥锁。
- 服务员来电
std::condition_variable::wait
.
- 服务员检查
while (!_Pred())
和 viola! 谓词为真。
- 服务员甚至没有进入内部
wait
,因此无论通知者是否成功调用 std::condition_variable::notify_one
或尚未成功调用都无关紧要。
这就是 cppreference.com 要求背后的基本原理:
Even if the shared variable is atomic, it must be modified under the mutex in order to correctly publish the modification to the waiting thread.
注意这是对条件变量的一般规则,而不是对std::condition_variables
s的特殊要求(包括WindowsCONDITION_VARIABLE
s,POSIXpthread_cond_t
s等)。
回想一下,采用谓词的 wait
重载只是一个方便的函数,因此调用者不必处理虚假唤醒。标准 (§30.5.1/15) 明确表示此重载等效于 Microsoft 实现中的 while 循环:
Effects: Equivalent to:
while (!pred())
wait(lock);
简单的wait
行得通吗?你在调用 wait
之前和之后测试谓词吗?伟大的。你也在做同样的事情。或者你也在质疑void std::condition_variable::wait( std::unique_lock<std::mutex>& lock );
?
Windows 关键部分和 Slim Reader/Writer 锁是用户模式设施而不是内核对象是无关紧要的,与问题无关。有替代的实现。如果您有兴趣知道 Windows 如何设法自动释放 CS/SRWL 并进入等待状态(使用互斥体和事件的天真的 pre-Vista 用户模式实现做错了什么),那是另一个问题。
我研究了 std::condition_variable(lock,pred)
的 VC++ 实现,基本上,它看起来像这样:
template<class _Predicate>
void wait(unique_lock<mutex>& _Lck, _Predicate _Pred)
{ // wait for signal and test predicate
while (!_Pred())
wait(_Lck);
}
基本上,裸体 wait
调用 _Cnd_waitX
,调用 _Cnd_wait
,调用 do_wait
,调用 cond->_get_cv()->wait(cs);
(所有这些都在文件 cond.c).
cond->_get_cv()
returns Concurrency::details::stl_condition_variable_interface
.
如果我们转到文件 primitives.h
,我们会看到在 windows 7 及更高版本下,我们有 class stl_condition_variable_win7
,其中包含旧的好 win32 CONDITION_VARIABLE
,并且 wait
调用 __crtSleepConditionVariableSRW
。
做一些汇编调试,__crtSleepConditionVariableSRW
只是提取 SleepConditionVariableSRW
函数指针,然后调用它。
事情是这样的:据我所知,win32 CONDITION_VARIABLE
不是内核对象,而是用户模式对象。因此,如果某个线程通知了这个变量,但实际上没有线程在它上面休眠,那么你就失去了通知,线程将保持休眠状态,直到超时或其他线程通知它。一个小程序实际上可以证明这一点——如果你错过了通知点——你的线程将继续休眠,尽管其他线程通知了它。
我的问题是这样的:
一个线程等待条件变量和谓词 returns false。然后,发生上面解释的整个调用链。那时,另一个线程改变了环境,所以谓词将 return true and 通知条件变量。我们在原线程中传递了predicate,但是还是没有进入SleepConditionVariableSRW
——调用链很长
所以,虽然我们通知了条件变量并且条件变量上的谓词肯定会return为真(因为通知者这样做了),我们仍然阻塞条件变量,可能永远阻塞。
这是它的行为方式吗?这似乎是一个巨大的、丑陋的竞争条件等待发生。如果您通知条件变量并且它的谓词 return 为真 - 线程应该解除阻塞。但是,如果我们在检查谓词和进入睡眠之间处于中间状态——我们将永远被阻塞。 std::condition_variable::wait
不是原子函数。
标准是怎么说的?它真的是竞争条件吗?
你违反了合同,所以所有的赌注都被取消了。参见:http://en.cppreference.com/w/cpp/thread/condition_variable
TLDR:当您持有互斥锁时,谓词不可能被其他人更改。
你应该在持有互斥锁的同时更改谓词的基础变量并且你必须在调用std::condition_variable::wait
之前获取该互斥锁(两者都是因为wait
释放互斥量,因为这是约定)。
在您描述的场景中,更改发生在 之后 while (!_Pred())
发现谓词不成立但在 wait(_Lck)
有机会释放之前互斥体。这意味着您在不持有互斥体的情况下更改了谓词检查的内容。你违反了规则,竞争条件或无限等待仍然不是你能得到的最糟糕的 UB 类型。至少这些是本地的并且与您违反的规则相关,因此您可以找到错误...
如果您遵守规则,则:
- 服务员先取得互斥量
- 进入
std::condition_variable::wait
。 (回想一下,通知程序仍在等待互斥量。) - 检查谓词并发现它不成立。 (回想一下通知程序 仍然 在互斥体上等待。)
- 调用一些实现定义的魔法来释放互斥锁并等待,只有现在通知程序才能继续。
- 通知程序终于成功获取了互斥量。
- 通知者更改任何需要更改以使谓词成立的内容。
- 通知程序调用
std::condition_variable::notify_one
。
或:
- 通知程序获取互斥量。 (回想一下,服务员在尝试获取互斥锁时被阻止。)
- 通知者更改任何需要更改以使谓词成立的内容。 (回想一下,服务员还是被堵住了。)
- 通知程序释放互斥量。 (在途中的某个地方,服务员会调用
std::condition_variable::notify_one
,但是一旦互斥体被释放...) - 服务员获取互斥锁。
- 服务员来电
std::condition_variable::wait
. - 服务员检查
while (!_Pred())
和 viola! 谓词为真。 - 服务员甚至没有进入内部
wait
,因此无论通知者是否成功调用std::condition_variable::notify_one
或尚未成功调用都无关紧要。
这就是 cppreference.com 要求背后的基本原理:
Even if the shared variable is atomic, it must be modified under the mutex in order to correctly publish the modification to the waiting thread.
注意这是对条件变量的一般规则,而不是对std::condition_variables
s的特殊要求(包括WindowsCONDITION_VARIABLE
s,POSIXpthread_cond_t
s等)。
回想一下,采用谓词的 wait
重载只是一个方便的函数,因此调用者不必处理虚假唤醒。标准 (§30.5.1/15) 明确表示此重载等效于 Microsoft 实现中的 while 循环:
Effects: Equivalent to:
while (!pred()) wait(lock);
简单的wait
行得通吗?你在调用 wait
之前和之后测试谓词吗?伟大的。你也在做同样的事情。或者你也在质疑void std::condition_variable::wait( std::unique_lock<std::mutex>& lock );
?
Windows 关键部分和 Slim Reader/Writer 锁是用户模式设施而不是内核对象是无关紧要的,与问题无关。有替代的实现。如果您有兴趣知道 Windows 如何设法自动释放 CS/SRWL 并进入等待状态(使用互斥体和事件的天真的 pre-Vista 用户模式实现做错了什么),那是另一个问题。