Volatile and CreateThread

Volatile and CreateThread

本文关键字:CreateThread and Volatile      更新时间:2023-10-16

我刚才问了一个关于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上工作,但它将是低效的。首先,这迫使res1res2之前设置,尽管我们并不真正关心这一点……我们只需要在finished之前设置它们。在res1res2之间强制这种排序只会阻止有效的优化,从而损害性能。

对于更复杂的问题,您必须让每个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));
}

这将适用于所有体系结构。res1res2操作可以在编译器认为合适的情况下重新排序。执行原子释放确保所有非原子操作都有序完成,并且对执行原子获取的线程可见。

volatile只是防止编译器对声明的volatile的值进行假设(即优化)访问。换句话说,如果你声明一些volatile,你基本上是说它可能在任何时候改变它的值,因为编译器不知道的原因,所以任何时候你引用变量,它必须在那个时候查找值。
在这种情况下,编译器可能决定在处理器寄存器中实际缓存done的值,而不受其他地方可能发生的变化的影响——例如,线程2将其设置为true
我猜它在你的例子中起作用的原因是所有对done的引用实际上是done在内存中的真实位置。您不能期望情况总是如此,特别是当您开始要求更高级别的优化时。
此外,我想指出,使用volatile关键字进行同步是不合适的。它可能碰巧是原子的,但这只是由环境决定的。我建议您使用实际的线程同步结构,如wait conditionmutex

你能始终确保线程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 不是同步机制。它不保证原子性和排序。如果不能保证在共享资源上执行的所有操作都是原子性的,那么必须使用适当的锁!

最后,我强烈推荐阅读这些文章:

  1. Volatile:对于多线程编程几乎没用
  2. volatile应该获得原子性和线程可见性语义吗?