易失性和CreateThread

sig*_*sen 4 c++ volatile thread-safety

我刚刚问了一个涉及volatile:volatile array c ++的问题

然而,我的问题产生了关于什么volatile做的讨论.

有人声称在使用时CreateThread(),您不必担心volatiles.另一方面,Microsoft提供了一个volatile使用两个线程创建时的示例CreateThread().

我创建在Visual C下面的示例++速成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;
}
Run Code Online (Sandbox Code Playgroud)

如果done不是,你总能确定线程1会停止volatile吗?

Cor*_*son 9

什么volatile:

  • 阻止编译器优化任何访问.每次读/写都会产生读/写指令.
  • 阻止编译器使用其他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.
}
Run Code Online (Sandbox Code Playgroud)

在这里,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.
}
Run Code Online (Sandbox Code Playgroud)

这个简单的例子实际上可以在x86上运行,但效率很低.首先,这个力量res1要在之前设定res2,即使我们并不真正关心它......我们只是希望它们之前都设置好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));
}
Run Code Online (Sandbox Code Playgroud)

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