我刚刚问了一个涉及volatile的问题:volatile array c++
然而,我的问题产生了关于volatile
做什么的讨论。
有些人声称在使用CreateThread()
时,您不必担心volatiles
。
另一方面,当使用由volatile
创建的两个线程时,Microsoft提供了CreateThread()
的示例。
我在visual c ++ express 2010中创建了以下示例,如果您将done
标记为volatile
,则无关紧要
#include "targetver.h"
#include <Windows.h>
#include <stdio.h>
#include <iostream>
#include <tchar.h>
using namespace std;
bool done = false;
DWORD WINAPI thread1(LPVOID args)
{
while(!done)
{
}
cout << "Thread 1 done!\n";
return 0;
}
DWORD WINAPI thread2(LPVOID args)
{
Sleep(1000);
done = 1;
cout << "Thread 2 done!\n";
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
DWORD thread1Id;
HANDLE hThread1;
DWORD thread2Id;
HANDLE hThread2;
hThread1 = CreateThread(NULL, 0, thread1, NULL, 0, &thread1Id);
hThread2 = CreateThread(NULL, 0, thread2, NULL, 0, &thread2Id);
Sleep(4000);
CloseHandle(hThread1);
CloseHandle(hThread2);
return 0;
}
如果done
不是volatile
,您是否一定能确定线程1会停止?
答案 0 :(得分:9)
volatile
做了什么:
volatile
没有:
跨平台C ++中不应该依赖的一些非可移植行为:
volatile
以防止与其他指令重新排序。其他编译器没有,因为它会对优化产生负面影响。大多数时候,真正想要的是围栏(也称为障碍)和原子指令,如果你有一个C ++ 11编译器,或者通过编译器和否则就依赖于架构的功能。
Fences确保在使用时完成所有先前的读/写操作。在C ++ 11中,使用std::memory_order
枚举在各个点控制围栏。在VC ++中,您可以使用_ReadBarrier()
,_WriteBarrier()
和_ReadWriteBarrier()
来执行此操作。我不确定其他编译器。
在某些体系结构(如x86)上,fence只是阻止编译器重新排序指令的一种方法。在其他人身上,他们可能会发出一条指令,以防止CPU本身重新排序。
以下是不当使用的示例:
int res1, res2;
volatile bool finished;
void work_thread(int a, int b)
{
res1 = a + b;
res2 = a - b;
finished = true;
}
void spinning_thread()
{
while(!finished); // spin wait for res to be set.
}
此处,finished
允许在设置res
之前重新排序到!那么,volatile会阻止与其他volatile的重新排序,对吧?让我们尝试让每个res
变得不稳定:
volatile int res1, res2;
volatile bool finished;
void work_thread(int a, int b)
{
res1 = a + b;
res2 = a - b;
finished = true;
}
void spinning_thread()
{
while(!finished); // spin wait for res to be set.
}
这个简单的例子实际上可以在x86上运行,但效率很低。首先,这会强制res1
在res2
之前设置,即使我们并不真正关心它......我们只想在finished
之前设置它们。在res1
和res2
之间强制执行此排序只会阻止有效的优化,从而影响性能。
对于更复杂的问题,您必须使每次写volatile
。这会使你的代码膨胀,非常容易出错,并且变得很慢,因为它会阻止比你真正想要的更多的重新排序。
这是不现实的。所以我们使用栅栏和原子。它们允许完全优化,并且只保证内存访问将在围栏点完成:
int res1, res2;
std::atomic<bool> finished;
void work_thread(int a, int b)
{
res1 = a + b;
res2 = a - b;
finished.store(true, std::memory_order_release);
}
void spinning_thread()
{
while(!finished.load(std::memory_order_acquire));
}
这适用于所有架构。 res1
和res2
操作可以在编译器认为合适时重新排序。执行原子发布可确保所有非原子操作都被排序完成,并且对执行原子获取的线程可见。
答案 1 :(得分:3)
volatile
只是阻止编译器对声明为volatile
的值进行假设(读取:优化)访问。换句话说,如果你声明一些volatile
,你基本上是说它可能随时因编译器不知道的原因改变它的值,所以每当你引用变量时它必须查找该值的值。时间。
在这种情况下,编译器可能决定在处理器寄存器中实际缓存done
的值,而与其他地方可能发生的更改无关 - 即线程2将其设置为true
。
我猜它在你的例子中起作用的原因是done
的所有引用实际上都是done
在内存中的真实位置。您不能指望始终如此,尤其是当您开始请求更高级别的优化时。
另外,我想指出,volatile
关键字不适合用于同步。它可能恰好是原子的,但仅限于环境。我建议你使用像wait condition
或mutex
这样的实际线程同步结构。请参阅http://software.intel.com/en-us/blogs/2007/11/30/volatile-almost-useless-for-multi-threaded-programming/以获得精彩的解释。
答案 2 :(得分:1)
如果没有完成,你总是可以确定线程1会停止
volatile
?
始终?不可以。但在这种情况下,done
的分配属于同一模块,而while
循环可能未被优化。取决于MSVC如何执行其优化。
通常,使用volatile
声明它更安全,以避免优化的不确定性。
答案 3 :(得分:1)
这比你想象的要糟糕,实际上 - 一些编译器可能会认为该循环是无操作或infinite loop,消除无限循环情况,并使其立即返回无论做什么是。编译器当然可以自由地将done
保留在本地CPU寄存器中,并且永远不会在循环中访问其更新值。您必须使用适当的内存屏障,或者使用易失性标志变量(这在某些CPU架构上技术上是不够的),或者像这样的标志使用锁保护变量。
答案 4 :(得分:0)
在linux,g ++ 4.1.2上编译,我输入了相当于你的例子:
#include <pthread.h>
bool done = false;
void* thread_func(void*r) {
while(!done) {};
return NULL;
}
void* write_thread_func(void*r) {
done = true;
return NULL;
}
int main() {
pthread_t t1,t2;
pthread_create(&t1, NULL, thread_func, NULL);
pthread_create(&t2, NULL, write_thread_func, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
}
当使用-O3编译时,编译器缓存了该值,因此它检查了一次,然后在第一次没有完成时进入无限循环。
然而,我将程序更改为以下内容:
#include <pthread.h>
bool done = false;
pthread_mutex_t mu = PTHREAD_MUTEX_INITIALIZER;
void* thread_func(void*r) {
pthread_mutex_lock(&mu);
while(!done) {
pthread_mutex_unlock(&mu);
pthread_mutex_lock(&mu);
};
pthread_mutex_unlock(&mu);
return NULL;
}
void* write_thread_func(void*r) {
pthread_mutex_lock(&mu);
done = true;
pthread_mutex_unlock(&mu);
return NULL;
}
int main() {
pthread_t t1,t2;
pthread_create(&t1, NULL, thread_func, NULL);
pthread_create(&t2, NULL, write_thread_func, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
}
虽然这仍然是一个旋转(它只是反复锁定/解锁互斥锁),但编译器将调用更改为始终在从pthread_mutex_unlock返回后检查done的值,从而使其正常工作。
进一步的测试表明,调用任何外部函数似乎会导致它重新检查变量。
答案 5 :(得分:0)
volatile
不是同步机制。 不会保证原子性和排序。如果您不能保证在共享资源上执行的所有操作都是原子操作,那么必须使用正确的锁定!
最后,我强烈建议您阅读这些文章: