易失性和CreateThread

时间:2011-07-28 21:50:53

标签: c++ thread-safety volatile

我刚刚问了一个涉及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会停止?

6 个答案:

答案 0 :(得分:9)

volatile做了什么:

  • 阻止编译器优化任何访问。每次读/写都会产生读/写指令。
  • 阻止编译器将访问与其他易失性重新排序。

volatile没有:

  • 使访问成为原子。
  • 防止编译器使用非易失性访问进行重新排序。
  • 从另一个线程中可见的一个线程进行更改。

跨平台C ++中不应该依赖的一些非可移植行为:

  • VC ++扩展了volatile以防止与其他指令重新排序。其他编译器没有,因为它会对优化产生负面影响。
  • x86使指针大小和较小变量的读/写对齐原子,并立即对其他线程可见。其他架构则没有。

大多数时候,真正想要的是围栏(也称为障碍)和原子指令,如果你有一个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上运行,但效率很低。首先,这会强制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操作可以在编译器认为合适时重新排序。执行原子发布可确保所有非原子操作都被排序完成,并且对执行原子获取的线程可见。

答案 1 :(得分:3)

volatile只是阻止编译器对声明为volatile的值进行假设(读取:优化)访问。换句话说,如果你声明一些volatile,你基本上是说它可能随时因编译器不知道的原因改变它的值,所以每当你引用变量时它必须查找该值的值。时间。
在这种情况下,编译器可能决定在处理器寄存器中实际缓存done的值,而与其他地方可能发生的更改无关 - 即线程2将其设置为true
我猜它在你的例子中起作用的原因是done的所有引用实际上都是done在内存中的真实位置。您不能指望始终如此,尤其是当您开始请求更高级别的优化时。
另外,我想指出,volatile关键字不适合用于同步。它可能恰好是原子的,但仅限于环境。我建议你使用像wait conditionmutex这样的实际线程同步结构。请参阅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 不是同步机制。 不会保证原子性和排序。如果您不能保证在共享资源上执行的所有操作都是原子操作,那么必须使用正确的锁定

最后,我强烈建议您阅读这些文章:

  1. Volatile: Almost Useless for Multi-Threaded Programming
  2. Should volatile Acquire Atomicity and Thread Visibility Semantics?