我用C ++编写了一个Windows程序,它有时使用两个线程:一个后台线程用于执行耗时的工作;和另一个用于管理图形界面的线程。这样,程序仍然响应用户,这需要能够中止某个操作。线程通过共享bool
变量进行通信,当GUI线程通知工作线程中止时,该变量设置为true
。以下是实现此行为的代码(我已经删除了不相关的部分):
class ProgressBarDialog : protected Dialog {
/**
* This points to the variable which the worker thread reads to check if it
* should abort or not.
*/
bool volatile* threadParameterAbort_;
...
BOOL CALLBACK ProgressBarDialog::DialogProc( HWND dialog, UINT message,
WPARAM wParam, LPARAM lParam ) {
switch( message ) {
case WM_COMMAND :
switch ( LOWORD( wParam ) ) {
...
case IDCANCEL :
case IDC_BUTTON_CANCEL :
switch ( progressMode_ ) {
if ( confirmAbort() ) {
// This causes the worker thread to be aborted
*threadParameterAbort_ = true;
}
break;
}
return TRUE;
}
}
return FALSE;
}
...
};
class CsvFileHandler {
/**
* This points to the variable which is set by the GUI thread when this
* thread should abort its execution.
*/
bool volatile* threadParamAbort_;
...
ParseResult parseFile( ItemList* list ) {
ParseResult result;
...
while ( readLine( &line ) ) {
if ( ( threadParamAbort_ != NULL ) && *threadParamAbort_ ) {
break;
}
...
}
return result;
}
...
};
两个线程中的 threadParameterAbort_
指向在结构中声明的bool
变量,该变量在创建时传递给工作线程。它被声明为
bool volatile abortExecution_;
我的问题是:我需要在这里使用volatile
,上面的代码是否足以确保程序是线程安全的?我在这里证明使用volatile
的理由(参见this question的背景知识)的理由是它会:
阻止读取*threadParameterAbort_
以使用缓存,而是从内存中获取值,
阻止编译器因优化而删除工作线程中的if
子句。
(以下段落仅涉及程序的线程安全性,不,我再说一遍,不涉及声称{{1}以任何方式提供任何确保线程安全的方法。)据我所知,它应该是线程安全的,因为volatile
变量的设置应该在大多数(如果不是全部)架构中都是原子操作。但我可能是错的。而且我也担心编译器是否可能重新排序指令,例如破坏线程安全性。但最好是安全(没有双关语)而不是抱歉。
修改:
我的措辞中的一个小错误使问题看起来好像我在询问bool
是否足以确保线程安全。这不是我的意图 - volatile
确实无法以任何方式确保线程安全 - 但我要问的是,上面提供的代码是否表现出正确的行为以确保程序是线程安全的。
答案 0 :(得分:14)
你不应该依赖volatile来保证线程安全,这是因为即使编译器会保证变量总是从内存中读取(而不是寄存器缓存),在多处理器环境中也会有内存障碍是必需的。
而是在共享内存周围使用正确的锁。像关键部分这样的锁通常非常轻量级,并且在没有争用的情况下可能都是用户实现的。它们还将包含必要的记忆障碍。
Volatile仅应用于内存映射IO,其中多个读取可能返回不同的值。类似地,对于内存映射写入。
答案 1 :(得分:9)
Wikipedia说得很好。
在C中,因此在C ++中, volatile关键字旨在允许访问内存映射设备,允许在setjmp之间使用变量,允许在信号处理程序中使用sig_atomic_t变量
对volatile变量的操作是 不是原子的,也不是建立适当的 发生在关系之前 穿线。这是根据 相关标准(C,C ++,POSIX, WIN32),这是事实 对于绝大多数当前 实现。 不稳定 关键字基本上是毫无价值的 便携式线程构造。
答案 2 :(得分:3)
volatile
对于C ++中的多线程来说既不必要也不充分。它禁用了完全可以接受的优化,但却无法强制执行所需的原子性。
编辑:我可能使用InterlockedIncrement
而不是使用临界区,这样可以减少开销,从而实现原子写入。
然而,我通常做的是将线程安全队列(或双端队列)连接起来作为线程的输入。当您有线程要做的事情时,您只需将描述该作业的数据包放入队列中,并且该线程尽可能地执行该操作。如果希望线程正常关闭,则将“关闭”数据包放入队列。如果你需要立即中止,你可以使用deque代替,然后将“abort”命令放在双端队列的前面。从理论上讲,这有一个缺点,就是它在完成当前任务之前不会中止线程。关于所有这些意味着您希望将每个任务保持与当前检查标志的频率大致相同的大小/等待时间范围。
这种通用设计避免了大量的IPC问题。
答案 3 :(得分:3)
关于我对昨天问题的回答,不,volatile
是不必要的。事实上,这里的多线程是无关紧要的。
while ( readLine( &line ) ) { // threadParamAbort_ is not local:
if ( ( threadParamAbort_ != NULL ) && *threadParamAbort_ ) {
- 阻止读取* threadParameterAbort_以使用缓存,而是从中获取值 记忆,和
- 阻止编译器删除worker中的if子句 线程由于优化。
醇>
函数readLine
是外部库代码,否则它调用外部库代码。因此,编译器不能假设有任何非局部变量它不会修改。一旦形成了指向对象(或其超级对象)的指针,它就可以被传递并存储在任何地方。编译器无法跟踪哪些指针在全局变量中结束,哪些指针不在。
因此,编译器假定readLine
拥有自己的私有static bool *
到threadParamAbort_
,并修改了该值。因此有必要从内存中重新加载。
答案 4 :(得分:1)
似乎在这里描述了相同的用例:Alexandrescu的volatile - Multithreaded Programmer's Best Friend。它表明,在这种情况下(创建标志)volatile
可以完美地使用。
所以,是的完全在这种情况下代码应该是正确的。 volative
将阻止两者 - 从缓存中读取并阻止编译器优化if
语句。
答案 5 :(得分:0)
虽然其他答案都是正确的,但我还建议您查看MSDN documentation for volatile
的“Microsoft特定”部分答案 6 :(得分:0)
我认为它可以正常工作(原子或非原子),因为你只是用它来取消后台操作。
volatile要做的主要事情是阻止从寄存器缓存或检索变量。你可以确定它来自主存。
所以,如果我是你,是的,我会将变量定义为volatile。
我认为最好假设需要volatile来确保变量在写入时由其他线程读取,然后尽快获得该变量的正确值,并确保IF未被优化掉(虽然我怀疑它会是)。
答案 7 :(得分:0)
好的,所以你已经对挥发性和线程安全性进行了足够的打击!但是......
您的特定代码的一个示例(尽管是在您的控制范围内)是您在一个'事务'中多次查看变量的事实:
if ( ( threadParamAbort_ != NULL ) && *threadParamAbort_ )
如果出于某种原因,在左侧和右侧之后删除了threadParamAbort_,那么您将取消引用已删除的指针。同样,如果你有控制权,这是不太可能的,但这是挥发性和原子性无法为你做的一个例子。