赋值运算符"="是原子的吗?

is assignment operator '=' atomic?

本文关键字:原子的 赋值运算符      更新时间:2023-10-16

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

//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之间切换打印。

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

这段代码不能保证在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_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_true现在是false other_bool现在是true

观察到,在这个序列的末尾,对is_true的更新丢失了,因为它被线程2覆盖了,线程2捕获了旧的is_true值。

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

不,不是.....您需要使用某种锁原语。根据不同的平台,你可以使用boost,或者如果是本机窗口,可以使用InterlockedCompareExchange。

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

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

对非自然对齐类型的读和写(例如,写跨4字节边界的DWORDs)不能保证是原子的。CPU可能不得不将这些读写作为多个总线事务进行,这可能允许另一个线程在读写过程中修改或查看数据。

这段代码的线程安全性不依赖于赋值的原子性。两个线程例程严格按顺序工作。没有竞争条件:thread_1将输出东西,直到得到某个随机数,之后它将离开"输出部分",让其他线程在其中工作。有几件事值得注意:

    rand()函数可能不是线程安全的(不是这里给出的代码中的问题)
  • 你不应该使用Win32函数CreateThread(),特别是当你使用CRT库函数(潜在地)利用全局变量时。使用_beginthreadx()代替。