赋值运算符"="是原子的吗?
is assignment operator '=' atomic?
我正在使用全局变量实现线程间通信。
//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_true
和other_bool
的4字节值将is_true
设置为true
。线程1读取0x00000000. - 线程2准备通过加载包含
is_true
和other_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()代替。
- 重载Singly Linked List中的赋值运算符
- 标准库类型的赋值运算符的引用限定符
- 标准::变体的赋值运算符
- C++ 中动态数组的赋值运算符
- 双链表C++的赋值运算符
- 派生自 std::exception 的类的赋值运算符
- 构造函数中的赋值运算符
- 为什么类的赋值运算符的返回类型通常是非常量(而不是常量)引用?
- 引用模板类型的赋值运算符需要非常量重载
- C++中动态分配的单向链表的赋值运算符 (MS Visual Studio 2015)
- C++的新增功能:创建 2D 数组并具有正确的赋值运算符:分割错误
- C++ 包含唯一指针成员变量的类的赋值运算符
- 使用带有下标运算符的赋值运算符将值分配给 std::map
- 是否有必要重载具有另一个类 B 的数据成员的类 A 的赋值运算符和复制构造函数?
- 链表的赋值运算符
- 在对类对象的赋值进行链接时获取垃圾值,使用按值返回类对象的赋值运算符重载
- 在基中使用派生类的赋值运算符
- 唯一指针中的赋值运算符错误
- 移动构造函数并移动类的赋值运算符
- { } 构造函数实现中的赋值运算符 => 错误