赋值运算符“="是原子的吗?
我正在使用全局变量实现线程间通信.
//全局变量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_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 覆盖,它捕获了 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
totrue
by loading the 4-byte value containingis_true
andother_bool
. Thread 1 reads 0x00000000. - Thread 2 prepares to set
other_bool
totrue
by loading the 4-byte value containingis_true
andother_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 nowtrue
andother_bool
is nowfalse
. - Thread 2 stores the updated value to memory.
is_true
is nowfalse
andother_bool
is nowtrue
.
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.
相关文章