赋值运算符“=”是原子操作吗?

28

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

//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;
}

问题

在上述代码中,我使用全局变量 volatile bool is_true 在 thread_1 和 thread_2 之间切换打印。

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


我更喜欢使用原子交换机制,但是我想不出会出现问题的情况... - Kerrek SB
@KerrekSB,这个场景怎么样?我只是即兴演示我的问题,:) - Alcott
嗯,我的意思是一系列的加载和存储操作足够破坏以使两个线程都进入临界区...通常应该能够演示这样的序列以展示为什么某些代码不正确。虽然我在这里看不到它,但我仍然不喜欢这段代码,但我无法证明为什么。 - Kerrek SB
我认为,你可以使用不止一个标志。第一个标志是用于从线程1向线程2发出信号,第二个标志是用于从线程2向线程1发出信号。然后,您将拥有两个每个都有一个写入器和许多读取器的变量。 - osgx
1
你不能在线程间通信时使用 volatile。(嗯,在某些非常特殊的情况下,你可以使用 - 但不适用于此处。) - curiousguy
类似但没有代码的问题:https://dev59.com/SHVD5IYBdhLWcg3wNIvc - Ciro Santilli OurBigBook.com
4个回答

阿里云服务器只需要99元/年,新老用户同享,点击查看详情
71

这段代码在Win32上不能保证线程安全,因为Win32只保证4字节和指针大小的值对齐时才是原子性的。bool不能保证是这些类型之一。(通常是1字节类型)

为了说明它可能失败的实际示例:

假设 bool 是一个1字节类型。同时假设你的 is_true 变量恰好存储在另一个 bool 变量 (我们称之为 other_bool) 的相邻位置上,这样它们两个共享同一个4字节行。为了具体起见,假设 is_true 在地址0x1000处,other_bool 在地址0x1001处。假设两个值最初都是 false,并且一个线程决定在另一个线程尝试更新 other_bool 的同时更新 is_true。可以出现以下操作序列:

  • 线程1准备通过加载包含 is_trueother_bool 的4字节值将 is_true 设置为 true。线程1读取0x00000000。
  • 线程2准备通过加载包含 is_trueother_bool 的4字节值将 other_bool 设置为 true。线程2读取0x00000000。
  • 线程1更新对应于 is_true 的4字节值中的字节,生成0x00000001。
  • 线程2更新对应于 other_bool 的4字节值中的字节,生成0x00000100。
  • 线程1将更新后的值存储到内存中。is_true 现在是 true,而 other_bool 现在是 false
  • 线程2将更新后的值存储到内存中。现在,is_truefalse,而other_booltrue
  • 观察到在此序列结尾处,对is_true的更新被丢失了,因为它被线程2覆盖,线程2捕获了一个旧的is_true值。

    这种类型的错误在x86上通常不会造成太大问题,因为它支持字节级别的更新并具有非常紧密的内存模型。其他Win32处理器则没有那么宽容。例如,RISC芯片通常不支持字节级别的更新,即使支持,它们通常也有非常弱的内存模型。


    3
    顺便问一下,使用“volatile”关键字能否强制编译器按目标架构以线程安全的方式对齐变量(即将“bool”值存储在32位x86处理器的4字节单元中)?这将解决问题。一些编译器是否会采用此技巧? - Pavel Gatilov
    1
    根据最新的草案标准,编译器必须防止这种行为。 - curiousguy
    2
    这个解释听起来不错,但有点令人担忧:如果我有两个完全不相关的全局变量使用不同的线程,那么根据你的推理,我仍然应该担心这两个线程会混淆彼此的数据。这听起来不太对。 - Kerrek SB
    4
    当你操作亚原子粒子时,生活会变得很奇怪。 - Raymond Chen
    2
    @RaymondChen:是的。幸运的是,这不是关于QFT的问题,而是关于C++的问题! - Kerrek SB
    显示剩余6条评论

    7
    不,不是这样的……你需要使用某种锁定原语。根据平台的不同,可以使用boost或者类似InterlockedCompareExchange这样的本机windows函数。 实际上,在您的情况下,您可能需要使用一些线程安全的事件机制,以便“信号”您的其他线程开始执行您想要的操作。

    2
    但是,OP的代码有什么问题呢?你能想出一个场景,在这个场景中会出现错误吗? - Kerrek SB
    @KerrekSB,不好意思,OP是什么意思? - Alcott
    2
    @Alcott:"OP" 的意思是“原帖发布者”,指的是提问的人。在这种情况下,就是你自己了。 :-) - ruakh
    锁原语是用于存在2个或多个线程之间竞争的情况。在此处给出的代码中并不存在这种情况。thread_1在关键部分工作,直到它滚动某个随机数。然后它更改全局变量并离开。只有在此之后,其他线程才能进入。 - Pavel Zhuravlev
    1
    @curiousguy: 错过了重点...我的问题是,“您如何打破原始帖子的代码?” 您能否编写导致错误行为的执行顺序? - Kerrek SB
    显示剩余3条评论

    4
    在所有现代处理器上,您可以假设自然对齐的本地类型的读取和写入是原子性的。只要内存总线至少与正在读取或写入的类型一样宽,CPU 就会在单个总线事务中读取和写入这些类型,从而使其他线程无法以半完成状态查看它们。在 x86 和 x64 上,不能保证大于八字节的读取和写入是原子性的。这意味着 SSE(流式 SIMD 扩展)寄存器的 16 字节读取和写入以及字符串操作可能不是原子性的。 未自然对齐的类型的读取和写入——例如,跨越四字节边界的 DWORD 写入——不能保证是原子性的。CPU 可能必须将这些读取和写入作为多个总线事务进行,这可能允许另一个线程在读取或写入的中间修改或查看数据。

    -2

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

    • rand()函数可能不是线程安全的(尽管在此处给出的代码中并非问题)
    • 当使用CRT库函数时(潜在地)利用全局变量时,不应使用Win32函数CreateThread()。应改用_beginthreadex()。

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,