Volatile and CreateThread
Volatile and CreateThread
我刚才问了一个关于volatile的问题:volatile array c++
然而,我的问题引发了关于volatile
做什么的讨论。
有人声称,当使用CreateThread()
时,你不必担心volatiles
。另一方面,Microsoft给出了volatile
的一个示例,其中使用了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将停止吗?
volatile
做什么:
- 防止编译器优化出任何访问。每次读/写都会产生一条读/写指令。
- 防止编译器将访问与其他volatile 重新排序。
volatile
没有:
- 防止编译器使用非易失性访问重新排序。 使一个线程的更改在另一个线程中可见。
在跨平台c++中不应该依赖的一些不可移植的行为:
- vc++扩展了
volatile
,以防止任何与其他指令的重新排序。其他编译器不会,因为它会对优化产生负面影响。 - x86使指针大小和更小的变量的读写对齐原子化,并立即对其他线程可见。
大多数时候,人们真正想要的是栅栏(也称为屏障)和原子指令,如果你有一个c++ 11编译器,或者通过编译器和体系结构相关的函数,它们是可用的。
fence确保在使用时,之前的所有读/写操作都将完成。在c++ 11中,使用std::memory_order
枚举在不同的点上控制栅栏。在vc++中,您可以使用_ReadBarrier()
, _WriteBarrier()
和_ReadWriteBarrier()
来完成此操作。我不确定其他的编译器。
在像x86这样的体系结构中,栅栏仅仅是防止编译器重新排序指令的一种方法。在其他情况下,它们可能会发出一条指令来阻止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
之前被重新排序为 !易失性阻止了与其他易失性的重新排序,对吧?让我们试着让每个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
操作可以在编译器认为合适的情况下重新排序。执行原子释放确保所有非原子操作都有序完成,并且对执行原子获取的线程可见。
volatile
只是防止编译器对声明的volatile
的值进行假设(即优化)访问。换句话说,如果你声明一些volatile
,你基本上是说它可能在任何时候改变它的值,因为编译器不知道的原因,所以任何时候你引用变量,它必须在那个时候查找值。
在这种情况下,编译器可能决定在处理器寄存器中实际缓存done
的值,而不受其他地方可能发生的变化的影响——例如,线程2将其设置为true
。
我猜它在你的例子中起作用的原因是所有对done
的引用实际上是done
在内存中的真实位置。您不能期望情况总是如此,特别是当您开始要求更高级别的优化时。
此外,我想指出,使用volatile
关键字进行同步是不合适的。它可能碰巧是原子的,但这只是由环境决定的。我建议您使用实际的线程同步结构,如wait condition
或mutex
。
你能始终确保线程1将停止,如果没有完成
volatile
?
总吗?不。但是在这种情况下,对done
的赋值在同一个模块中,while
循环将可能不会被优化出来。取决于MSVC如何执行其优化。
通常,使用volatile
声明它更安全,以避免优化中的不确定性。
实际上,这比您想象的更糟-一些编译器可能会决定该循环是无操作或无限循环,消除无限循环的情况,并使其立即返回,无论做了什么。编译器当然可以自由地将done
保存在本地CPU寄存器中,并且永远不会在循环中访问其更新后的值。您必须使用适当的内存屏障,或者使用易失性标志变量(在某些CPU架构上,这在技术上是不够的),或者为这样的标志使用受锁保护的变量。
在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的值,从而使其正常工作。
进一步的测试表明,调用任何外部函数似乎都会导致它重新检查变量。
volatile
不是同步机制。它不保证原子性和排序。如果不能保证在共享资源上执行的所有操作都是原子性的,那么必须使用适当的锁!
最后,我强烈推荐阅读这些文章:
- Volatile:对于多线程编程几乎没用 volatile应该获得原子性和线程可见性语义吗?
- C++核心准则 C35 对于接口类"A base class destructor should be either public and virtual, or protected and nonv
- 为什么C++逐位AND运算符在不同大小的操作数中表现为这样
- 为什么 Clang 不允许"and"作为函数名称?
- 位阵列上的快速AND运算
- 是否可以在 C++03 中定义'move-and-swap idiom'等效项
- BoostPython and CMake
- OpenSSL BIO and SSL_read
- Gurobi GRBModel and GRBmodel in C++
- std::visit and std::variant usage
- SHBrowseForFolder with BIF_BROWSEFORCOMPUTER and SHGetPathFr
- Directx12 and keystrokes
- different between int **arr =new int [ n]; and int a[i][j]?
- C++ getenv and setenv
- Inference pytorch C++ with alexnet and cv::imread image
- Visual Studio 2019 C++ and std::filesystem
- 保证逻辑 AND 表达式中的函数调用
- python ctypes and C++ pointers
- DllMain w/ createthread and printf
- DLL CreateThread, DisableThreadLibraryCalls and _beginthread
- Volatile and CreateThread