赋值运算符'='原子?

时间:2011-11-28 02:01:33

标签: c++ multithreading thread-safety communication visual-c++

我正在使用全局变量实现线程间通信。

//global var
volatile bool is_true = true;

//thread 1
void thread_1()
{
    while(1){
        int rint = rand() % 10;
        if(is_true) {
            cout << "thread_1: "<< rint <<endl;  //thread_1 prints some stuff
            if(rint == 3)
                is_true = false;  //here, tells thread_2 to start printing stuff
        }
    }
}

//thread 2
void thread_2()
{
    while(1){
        int rint = rand() % 10;
        if(! is_true) {  //if is_true == false
            cout << "thread_1: "<< rint <<endl;  //thread_2 prints some stuff
            if(rint == 7)  //7
                is_true = true;  //here, tells thread_1 to start printing stuff
        }
    }
}

int main()
{
    HANDLE t1 = CreateThread(0,0, thread_1, 0,0,0);
    HANDLE t2 = CreateThread(0,0, thread_2, 0,0,0);
    Sleep(9999999);
    return 0;
}

问题

在上面的代码中,我使用全局var volatile bool is_true来切换thread_1和thread_2之间的打印。

我想知道在这里使用赋值操作是否是线程安全的

4 个答案:

答案 0 :(得分:57)

此代码不保证在Win32上是线程安全的,因为Win32仅为正确对齐的4字节和指针大小的值保证原子性。 bool不保证是其中一种类型。 (通常为1字节类型。)

对于那些需要一个如何失败的实际例子的人:

假设bool是1字节类型。还假设您的is_true变量恰好存储在另一个bool变量旁边(让我们称之为other_bool),这样它们就会共享相同的4字节行。具体来说,假设is_true位于地址0x1000,other_bool位于地址0x1001。假设两个值最初都是false,并且一个线程决定在另一个线程尝试更新is_true的同时更新other_bool。可能会发生以下操作序列:

  • 线程1通过加载包含is_truetrue的4字节值,准备将is_true设置为other_bool。线程1读取0x00000000。
  • 线程2通过加载包含other_booltrue的4字节值,准备将is_true设置为other_bool。线程2读取0x00000000。
  • 线程1更新与is_true对应的4字节值中的字节,产生0x00000001。
  • 线程2更新与other_bool对应的4字节值中的字节,产生0x00000100。
  • 线程1将更新后的值存储到内存中。 is_true现在是trueother_bool现在是false
  • 线程2将更新后的值存储到内存中。 is_true现在是falseother_bool现在是true

观察到此序列结束时,is_true的更新已丢失,因为它被线程2覆盖,线程2捕获旧值is_true

碰巧x86非常容忍这种类型的错误,因为它支持字节粒度更新并且具有非常紧凑的内存模型。其他Win32处理器并不宽容。例如,RISC芯片通常不支持字节粒度更新,即使它们存在,它们通常也具有非常弱的内存模型。

答案 1 :(得分:7)

不,它不是.....你需要使用某种类型的锁定原语。根据平台的不同,您可以使用升级版本,也可以使用本机窗口,例如InterlockedCompareExchange。

事实上,在你的情况下,你可能会使用一些线程安全事件机制,这样你就可以“发出信号”你的其他线程开始做你想做的事。

答案 2 :(得分:4)

在所有现代处理器上,您可以假设自然对齐的本机类型的读取和写入是原子的。只要存储器总线至少与读取或写入的类型一样宽,CPU就会在单个总线事务中读取和写入这些类型,这使得其他线程无法在半完成状态下看到它们。在x86和x64上,不能保证读取和写入大于比八个字节都是原子的。这意味着流式SIMD扩展(SSE)寄存器和字符串操作的16字节读写可能不是原子的。

读取和写入非自然对齐的类型 - 例如,编写跨越四字节边界的DWORD - 不保证是原子的。 CPU可能必须作为多个总线事务执行这些读取和写入操作,这可能允许另一个线程在读取或写入过程中修改或查看数据。

答案 3 :(得分:-2)

这段代码的线程安全性不依赖于赋值的原子性。两个线程例程都严格依次工作。没有竞争条件:thread_1将输出内容,直到获得某个随机数,之后它将离开“输出部分”并让其他线程在其中工作。 但有几点值得注意:

  • rand()函数可能不是线程安全的(虽然这里给出的代码中没有问题)
  • 你不应该使用Win32函数CreateThread(),特别是当你使用CRT libraly函数时(可能)利用全局变量。请改用_beginthreadex()。