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

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

//全局变量volatile bool is_true = true;//线程1无效线程_1(){而(1){int rint = rand() % 10;如果(is_true){cout<<线程_1:"<<rint<<endl;//thread_1 打印一些东西如果(rint == 3)is_true = false;//这里,告诉thread_2开始打印东西}}}//线程2void thread_2(){而(1){int rint = rand() % 10;if(!is_true) {//if is_true == falsecout<<线程_1:"<<rint<<endl;//thread_2 打印一些东西if(rint == 7)//7is_true = 真;//这里,告诉thread_1开始打印东西}}}int main(){处理 t1 = CreateThread(0,0, thread_1, 0,0,0);处理 t2 = CreateThread(0,0, thread_2, 0,0,0);睡眠(9999999);返回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 现在是 trueother_bool 现在是 false.
  • 线程 2 将更新后的值存储到内存中.is_true 现在是 falseother_bool 现在是 true.

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

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

I'm implementing Inter-Thread Communication using global variable.

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

Question

In the code above, I use a global var volatile bool is_true to switch printing between thread_1 and thread_2.

I wonder whether it is thread-safe to use assignment operation here?

解决方案

This code is not guaranteed to be thread-safe on Win32, since Win32 guarantees atomicity only for properly-aligned 4-byte and pointer-sized values. bool is not guaranteed to be one of those types. (It is typically a 1-byte type.)

For those who demand an actual example of how this could fail:

Suppose that bool is a 1-byte type. Suppose also that your is_true variable happens to be stored adjacent to another bool variable (let's call it other_bool), so that both of them share the same 4-byte line. For concreteness, let's say that is_true is at address 0x1000 and other_bool is at address 0x1001. Suppose that both values are initially false, and one thread decides to update is_true at the same time another thread tries to update other_bool. The following sequence of operations can occur:

  • Thread 1 prepares to set is_true to true by loading the 4-byte value containing is_true and other_bool. Thread 1 reads 0x00000000.
  • Thread 2 prepares to set other_bool to true by loading the 4-byte value containing is_true and other_bool. Thread 2 reads 0x00000000.
  • Thread 1 updates the byte in the 4-byte value corresponding to is_true, producing 0x00000001.
  • Thread 2 updates the byte in the 4-byte value corresponding to other_bool, producing 0x00000100.
  • Thread 1 stores the updated value to memory. is_true is now true and other_bool is now false.
  • Thread 2 stores the updated value to memory. is_true is now false and other_bool is now true.

Observe that at the end this sequence, the update to is_true was lost, because it was overwritten by thread 2, which captured an old value of is_true.

It so happens that x86 is very forgiving of this type of error because it supports byte-granular updates and has a very tight memory model. Other Win32 processors are not as forgiving. RISC chips, for example, often do not support byte-granular updates, and even if they do, they usually have very weak memory models.

相关文章