Chinaunix

标题: 再论int变量赋值的原子性 [打印本页]

作者: freedon    时间: 2010-10-26 11:22
标题: 再论int变量赋值的原子性
前几天做项目,遇到了多线程读写全局int变量是否需要进行保护的问题,于是搜索到了四年前几位前辈对这个问题的精彩的讨论:
http://www.chinaunix.net/jh/23/804826.html

整篇帖子看下来,给我醍醐灌顶的感觉,实在是受益非浅;而这种做技术来不得一丝马虎的执着才是一个程序员所应该具备的执着。

唯一美中不足的,是整个帖了结束的时候,没有一个十分明确的结论到底int变量的多线程操作不加保护有没有危险,如果有危险,是在什么样的条件?而我遇到的问题很单一:CPU为ARM9,在内存没有对齐的情况下,会不会存在一个线程给一个int变量赋值没有结束而被另一个线程读走的危险?

以下是我的实验过程

实验代码:

  1. #include <stdio.h>
  2. #include <unistd.h>
  3. #include <pthread.h>
  4. struct STRU
  5. {
  6.     char a;
  7.     int  b;
  8. }__attribute__((packed));
  9. struct STRU v;

  10. void threadfun1(void)
  11. {
  12.     int i = 0;
  13.     while(1)
  14.     {
  15.         if(i%2 == 0)
  16.         {
  17.             v.b = 0x55555555;
  18.         }
  19.         else
  20.         {
  21.             v.b = 0xaaaaaaaa;
  22.         }
  23.         i++;
  24.     }      
  25. }

  26. void threadfun2(void)
  27. {   int i;
  28.    
  29.     while(1)
  30.     {   
  31.         i = v.b;
  32.         if((i != 0x55555555) && (i != 0xaaaaaaaa))
  33.         {   
  34.             printf("v.b = %x\n", i);
  35.         }
  36.     }
  37. }

  38. void main(void)
  39. {
  40.     pthread_t tidFun1;
  41.     pthread_t tidFun2;
  42.     pthread_create(&tidFun1, NULL, threadfun1, NULL);
  43.     pthread_create(&tidFun2, NULL, threadfun2, NULL);
  44.     while(1)
  45.     {
  46.         sleep(5);
  47.     }
  48. }
复制代码


如上,结构体struct STRU中的成员变量b,因为使用了宏__attribute__((packed)),所以采取的是字节对齐的方式而不是内存对齐的方式,用arm-linux-gcc来编译并反汇编,线程1的函数代码如下:

  1. 00008520 <threadfun1>:
  2.     8520:       e1a0c00d        mov     ip, sp
  3.     8524:       e92dd800        stmdb   sp!, {fp, ip, lr, pc}
  4.     8528:       e24cb004        sub     fp, ip, #4      ; 0x4
  5.     852c:       e24dd004        sub     sp, sp, #4      ; 0x4
  6.     8530:       e3a03000        mov     r3, #0  ; 0x0
  7.     8534:       e50b3010        str     r3, [fp, #-16]
  8.     8538:       e51b3010        ldr     r3, [fp, #-16]
  9.     853c:       e2233001        eor     r3, r3, #1      ; 0x1
  10.     8540:       e2033001        and     r3, r3, #1      ; 0x1
  11.     8544:       e20330ff        and     r3, r3, #255    ; 0xff
  12.     8548:       e3530000        cmp     r3, #0  ; 0x0
  13.     854c:       0a00000d        beq     8588 <threadfun1+0x68>
  14.     8550:       e59f2094        ldr     r2, [pc, #148]  ; 85ec <.text+0x1a8>
  15.     8554:       e3a03000        mov     r3, #0  ; 0x0
  16.     8558:       e3833055        orr     r3, r3, #85     ; 0x55
  17.     855c:       e5c23001        strb    r3, [r2, #1]
  18.     8560:       e3a03000        mov     r3, #0  ; 0x0
  19.     8564:       e3833055        orr     r3, r3, #85     ; 0x55
  20.     8568:       e5c23002        strb    r3, [r2, #2]
  21.     856c:       e3a03000        mov     r3, #0  ; 0x0
  22.     8570:       e3833055        orr     r3, r3, #85     ; 0x55
  23.     8574:       e5c23003        strb    r3, [r2, #3]
  24.     8578:       e3a03000        mov     r3, #0  ; 0x0
  25.     857c:       e3833055        orr     r3, r3, #85     ; 0x55
  26.     8580:       e5c23004        strb    r3, [r2, #4]
  27.     8584:       ea000014        b       85dc <threadfun1+0xbc>
  28.     8588:       e59f205c        ldr     r2, [pc, #92]   ; 85ec <.text+0x1a8>
  29.     858c:       e3a03000        mov     r3, #0  ; 0x0
  30.     8590:       e1e03003        mvn     r3, r3
  31.     8594:       e2033055        and     r3, r3, #85     ; 0x55
  32.     8598:       e1e03003        mvn     r3, r3
  33.     859c:       e5c23001        strb    r3, [r2, #1]
  34.     85a0:       e3a03000        mov     r3, #0  ; 0x0
  35.     85a4:       e1e03003        mvn     r3, r3
  36.     85a8:       e2033055        and     r3, r3, #85     ; 0x55
  37.     85ac:       e1e03003        mvn     r3, r3
  38.     85b0:       e5c23002        strb    r3, [r2, #2]
  39.     85b4:       e3a03000        mov     r3, #0  ; 0x0
  40.     85b8:       e1e03003        mvn     r3, r3
  41.     85bc:       e2033055        and     r3, r3, #85     ; 0x55
  42.     85c0:       e1e03003        mvn     r3, r3
  43.     85c4:       e5c23003        strb    r3, [r2, #3]
  44.     85c8:       e3a03000        mov     r3, #0  ; 0x0
  45.     85cc:       e1e03003        mvn     r3, r3
  46.     85d0:       e2033055        and     r3, r3, #85     ; 0x55
  47.     85d4:       e1e03003        mvn     r3, r3
  48.     85d8:       e5c23004        strb    r3, [r2, #4]
  49.     85dc:       e51b3010        ldr     r3, [fp, #-16]
  50.     85e0:       e2833001        add     r3, r3, #1      ; 0x1
  51.     85e4:       e50b3010        str     r3, [fp, #-16]
  52.     85e8:       eaffffd2        b       8538 <threadfun1+0x18>
  53.     85ec:       000109c5        andeq   r0, r1, r5, asr #19
复制代码


可以看到,对变量v.b的一个int赋值被分成了多条汇编语句,在目标机上运行,打印结果为以下多种结果
v.b = 55aaaaaa
v.b = 555555aa
v.b = aa555555
v.b = aaaa5555
......

由此可见,至少在arm9的CPU上,在非内存对齐的情况下,存在一个线程对全局的int变量赋值没有完成而被另一个线程读走,从而导致未知后果的可能性。

如果是在X86上又会如何?

以下是在X86上的汇编代码:
  1. 08048444 <threadfun1>:
  2. 8048444: 55 push %ebp
  3. 8048445: 89 e5 mov %esp,%ebp
  4. 8048447: 83 ec 10 sub $0x10,%esp
  5. 804844a: c7 45 fc 00 00 00 00 movl $0x0,0xfffffffc(%ebp)
  6. 8048451: 8b 45 fc mov 0xfffffffc(%ebp),%eax
  7. 8048454: 83 e0 01 and $0x1,%eax
  8. 8048457: 85 c0 test %eax,%eax
  9. 8048459: 75 0c jne 8048467 <threadfun1+0x23>
  10. 804845b: c7 05 15 97 04 08 55 movl $0x55555555,0x8049715
  11. 8048462: 55 55 55
  12. 8048465: eb 0a jmp 8048471 <threadfun1+0x2d>
  13. 8048467: c7 05 15 97 04 08 aa movl $0xaaaaaaaa,0x8049715
  14. 804846e: aa aa aa
  15. 8048471: 83 45 fc 01 addl $0x1,0xfffffffc(%ebp)
  16. 8048475: eb da jmp 8048451 <threadfun1+0xd>
复制代码


由上看到,X86对这种字节对齐的int变量也是只用了一条movl语句进行赋值,所以不会存在赋值到一半被另一个线程读走的情况,通过运行实验程序也证明了这个问题。

最后,虽然题目是“再论int变量赋值的原子性”,但如果其内容能达到前一帖子的百分之一二有用,我就心满意足了
作者: hdc1112    时间: 2010-10-26 11:44
好贴! 试试代码去。。
作者: 32242132    时间: 2010-10-26 12:52
不错. 话说当初就是那个帖子把我吸引到了CU.
  我的工作和UNIX毫无关系.但编程的东西大家是相通的.
作者: jimmyixy    时间: 2010-10-26 16:39
比较完美的总结 学习
作者: davelv    时间: 2010-10-26 17:23
汇编指令是可以被中断的。
作者: hdc1112    时间: 2010-10-26 17:28
弱问,为什么我把程序down下来,运行了,结果没有输出?!。。。
作者: freedon    时间: 2010-10-27 17:36
弱问,为什么我把程序down下来,运行了,结果没有输出?!。。。
hdc1112 发表于 2010-10-26 17:28



你是什么CPU?如果是在X86上,是没有输出的,因为是X86上是一条汇编指令就完成赋值了,保证了原子性。
作者: shujunz    时间: 2010-10-27 18:16
好贴!收藏了
作者: davelv    时间: 2010-10-27 21:03
你是什么CPU?如果是在X86上,是没有输出的,因为是X86上是一条汇编指令就完成赋值了,保证了原子性。 ...
freedon 发表于 2010-10-27 17:36

你这是仅仅对于单CPU的情况,如果是多核你这条就不管用了,movl是访问的内存,在这个核心读取的时候那边可能正在修改。。
在我的机器上就输出了v.b=0;
作者: freedon    时间: 2010-10-28 08:58
你这是仅仅对于单CPU的情况,如果是多核你这条就不管用了,movl是访问的内存,在这个核心读取的时候那边可 ...
davelv 发表于 2010-10-27 21:03



    多核的没试过,或许会存在你说的那个问题吧。
不过你v.b=0的情况,并不是多核的问题,应该是线程2先于线程1运行所致。
作者: phy0077    时间: 2010-10-28 09:14
struct STRU v;
你的v你malloc出来试试,看看是什么样子
作者: smalloc    时间: 2010-10-28 09:20
这是2个与多处理器相关的帖子
http://linux.chinaunix.net/bbs/viewthread.php?tid=1145097
http://bbs.chinaunix.net/viewthread.php?tid=1583949
作者: hdc1112    时间: 2010-10-28 20:20
回复 7# freedon

哦。我知道了。我的是x86.难怪没有输出。呵呵。我还以为是线程没开启。
作者: 小i    时间: 2010-10-28 20:33
靠,真没想到当年我这么神勇呐~

不过当年说话是够冲的...

另外好多年没搞C这些底层的东西,真是忘的差不多了...
作者: smalloc    时间: 2010-10-28 20:55
回复 14# 小i


    现在在忙些啥呢
作者: 小i    时间: 2010-10-28 21:02
回复  小i


    现在在忙些啥呢
smalloc 发表于 2010-10-28 20:55



现在用Java给电信运营商搞管理平台...

话说,Java搞多线程真的很爽。封装的非常完善的类库,原子操作,线程同步什么的很轻松就搞定了~
作者: 小i    时间: 2010-10-28 21:21
另外lz的问题,我认为是这样的:
1. 对于ARM来说,int的赋值不是原子的。你知道,ARM是RISC的,RISC就是麻烦
2. 对于X86单核来说,int赋值是准原子的。虽然中间可能被打断,但不影响赋值结果。
3. 对于X86多核来说,int赋值可能不是原子的... 如果没对齐的话。你知道,X86是CISC的,CISC有时候也会自找麻烦
4. 对于ARM多核来说... 厄... 当我什么都没说好了...
作者: freedon    时间: 2010-10-29 18:29
struct STRU v;
你的v你malloc出来试试,看看是什么样子
phy0077 发表于 2010-10-28 09:14



    咋,有什么区别吗?
作者: freedon    时间: 2010-10-29 18:33
靠,真没想到当年我这么神勇呐~   

不过当年说话是够冲的...

另外好多年 ...
小i 发表于 2010-10-28 20:33



    当年“行不改名,做不改姓”的isjfk怎么改名叫小i啦?哈哈
作者: koolcoy    时间: 2010-10-29 23:00
另外lz的问题,我认为是这样的:
1. 对于ARM来说,int的赋值不是原子的。你知道,ARM是RISC的,RISC就是麻 ...
小i 发表于 2010-10-28 21:21


对于x86来说,只有对齐的mov指令才具有原子性
作者: 小i    时间: 2010-10-30 13:20
当年“行不改名,做不改姓”的isjfk怎么改名叫小i啦?哈哈
freedon 发表于 2010-10-29 18:33



isjfk打酱油去了。小i是来灌水的...
作者: chenzhanyiczy    时间: 2010-10-30 21:08
另外lz的问题,我认为是这样的:
1. 对于ARM来说,int的赋值不是原子的。你知道,ARM是RISC的,RISC就是麻 ...
小i 发表于 2010-10-28 21:21




2是错误的,就算int对齐了,也是不准确的,仍然可能是不原子的
作者: bo_00    时间: 2010-10-31 01:19
不错,学习了。

主要看,对应CPU的汇编,对int赋值,是多句还是单句。
作者: bo_00    时间: 2010-10-31 01:22
弱弱的问问

在arm9上,即便没对齐,,是否也会出现,赋值途中被读取的情况呢?
也就是说arm9上,无论对齐与否,对int的赋值汇编语句,都是多句的呢?




欢迎光临 Chinaunix (http://bbs.chinaunix.net/) Powered by Discuz! X3.2