2011-11-28 248 views
24

我正在使用全局变量实现线程间通信。是赋值运算符'='原子?

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

我不知道这里是否使用赋值操作是线程安全的

+0

我宁愿使用原子交换原语,但我不能解决你会遇到问题的场景...... –

+0

@KerrekSB,这个场景?那么,我只是简单地展示了我的问题,:) – Alcott

+0

嗯,我的意思是一系列的加载和存储将被充分打破,使两个线程进入关键部分...通常应该能够展示这样一个序列以说明为什么某些代码不正确。虽然我在这里看不到它。我仍然不喜欢代码,但我无法证明为什么。 –

回答

54

此代码不保证在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_truetrue。线程1读取0x00000000。
  • 线程2准备通过加载包含is_trueother_bool的4字节值来设置other_booltrue。线程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芯片通常不支持字节粒度更新,即使他们这样做,他们通常也有非常弱的内存模型。

+0

关于对齐的很好的解释你的例子清楚地表明赋值不是原子的 –

+3

顺便说一句,'volatile'关键字不会强制编译器以线程安全的方式将变量与目标体系结构对齐(即存储对于32位x86处理器,4字节单元格中的“bool”值)?这可以解决问题。一些编译器是否会这样做? –

+1

@PavelGatilov根据最新的草案标准,编译器必须阻止这种行为 – curiousguy

7

不,它不是.....你需要使用某种锁定原语。根据平台,你可以使用boost,或者使用本地窗口,比如InterlockedCompareExchange。

事实上,在您的情况下,您可能需要使用某些线程安全的事件机制,以便您可以“通知”其他线程开始按照您的要求进行操作。

+2

虽然OP的代码有什么错误?你可以设计一个场景中断的地方吗? –

+0

@KerrekSB,对不起,OP?那是什么? – Alcott

+2

@Alcott:“OP”的意思是“原始海报”,如在提问的人中。在这种情况下,就是你。 :-) – ruakh

-1

这段代码的线程安全性不依赖于赋值的原子性。两个线程都依次严格运行。没有竞争条件:thread_1会输出一些东西,直到获得一定的随机数,然后它将离开'输出部分'并让其他线程在其中工作。 有一个值得注意的几件事情,但:

  • rand()函数可能不是线程安全的(而不是在代码这里给出虽然问题)
  • 你不应该使用Win32函数的CreateThread()特别是当您使用CRT潜在函数(潜在地)利用全局变量时。使用_beginthreadex()来代替。
+2

”_两个程序都严格依次工作。 ? – curiousguy

4

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

读取和写入不自然对齐的类型(例如,写入跨越四字节边界的DWORD)并不保证是原子性的。 CPU可能必须将这些读写操作做为多总线事务,这可能会允许另一个线程在读取或写入过程中修改或查看数据。