C語言/原子/編譯,你真的明白了嗎?


  版權申明:本文為博主窗戶(Colin Cai)原創,歡迎轉帖。如要轉貼,必須注明原文網址

  http://www.cnblogs.com/Colin-Cai/p/7668982.html 

  作者:窗戶

  QQ:6679072

  E-mail:6679072@qq.com

  說到原子,類似於以下的代碼可能人人都可以看出貓膩。

/* http://www.cnblogs.com/Colin-Cai */
#include <stdio.h>
#include <pthread.h>

int cnt = 0;
void* mythread(void* arg)
{
        int i;
        for(i=0;i<500000000;i++)
                cnt++;
        return NULL;
}

int main()
{
        pthread_t id, id2;

        pthread_create(&id, NULL, mythread, NULL);
        pthread_create(&id2, NULL, mythread, NULL);
        pthread_join(id, NULL);
        pthread_join(id2, NULL);
        printf("cnt = %d\n", cnt);

        return 0;
}

 

  我想大多數人都知道其結果未必會得到1000000000。

  測試一下吧。

linux-p94b:/tmp/testhere # gcc test1.c -lpthread
linux-p94b:/tmp/testhere # for((i=0;i<10;i++));do ./a.out ; done
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 958925625
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000

  可是真的知道貓膩了嗎?如果我編譯的時候優化一下呢?

linux-p94b:/tmp/testhere # gcc -O2 test1.c -lpthread
linux-p94b:/tmp/testhere # for((i=0;i<10;i++));do ./a.out ; done
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000

  運行速度一下子變的飛快,而且似乎都得到了10億。

  這里,mythread里cnt自加5億次被優化成了 cnt += 500000000

  那么當然快啊,可是似乎這與我們當初想測試原子有那么一些差異,一樣的代碼,不一樣的編譯,卻帶來了不同的結果。

  其實原因在於,我們這里代碼寫的不好,才沒有表達好我們當初的意思,我們是希望cnt真的自加5億次。那么怎么辦呢?其實很好辦,在cnt的定義前面加個volatile,那么這里對於cnt的自加則不會優化。很多時候,為什么我們優化前和優化后的結果不一樣,常常是因為寫代碼的人不明白程序的優化規則。在上個公司的時候,我很想臨走的時候再給大家做一個培訓,說說C語言的優化,同時說說我們平時寫的無意依賴於編譯的所謂垃圾代碼,但是直到離開,我還是沒有做此培訓。

  我們加了volatile試一下,

linux-p94b:/tmp/testhere # gcc -O2 test1.c -lpthread
linux-p94b:/tmp/testhere # for((i=0;i<10;i++));do ./a.out ; done
cnt = 635981117
cnt = 675792826
cnt = 522700646
cnt = 593410055
cnt = 544306380
cnt = 630888304
cnt = 580539893
cnt = 629360072
cnt = 555570127

  我們在cnt定義前加個volatile,效果果然就更明顯了,因為真的是自加5億次,導致問題的機會變多了。那么之前沒加volatile並優化編譯,會不會也有不得到10億的可能呢?

  我們首先要明白的是,這里的cnt++不是原子操作,中間有隨時調度的可能。

  5億次太多,我們就拿只自加1次為例即可說明,兩個線程都只自加1次,本來期待結果為2.

  cnt++在一般的處理器中至少有三條指令,我們用偽匯編來寫。  

  cnt -> reg  //把cnt從內存加載到寄存器reg

  reg+1 -> reg //寄存器reg自加1

  reg -> cnt     //把reg的內容寫入內存

  

  那么,

 

       (線程1)cnt -> reg

  (線程1)reg+1 -> reg

  (線程1)reg -> cnt

       (線程2)cnt -> reg

  (線程2)reg+1 -> reg

  (線程2)reg -> cnt

 

  理想中,我們認為處理器的執行是以上這樣,結果cnt里的值是2。

  但假設過程中發生了調度,指令執行的順序並非像以上這樣,假如變成了以下這樣

 

       (線程1)cnt -> reg

  (線程1)reg+1 -> reg  

       (線程2)cnt -> reg

  (線程2)reg+1 -> reg

  (線程2)reg -> cnt

  (線程1)reg -> cnt 

  我們再來算算,

  cnt = 0,  reg任意

       (線程1)cnt -> reg

  cnt = 0, reg =  0

  (線程1)reg+1 -> reg

  cnt = 0, reg = 1

  此處調度,reg = 1會被保存,並在重新調度回來之后有效,而cnt不會管

 

  調度之后

  cnt = 0, reg任意 

       (線程2)cnt -> reg

  cnt = 0, reg = 0

  (線程2)reg+1 -> reg

  cnt = 0, reg = 1

  (線程2)reg -> cnt

  cnt = 1, reg = 1

  此處又發生調度,reg會恢復之前保存的1,而cnt不會有任何變化

  所以在執行下一條指令前,

  cnt = 1, reg = 1

  (線程1)reg -> cnt 

  cnt = 1, reg = 1

  

  我們可以看到,結果成了1,而不是2,這就是非原子操作導致的結果,其實之前優化成cnt += 500000000本身也依然有此問題,只是難以觀察的到。

  雖然x++不是原子,但是我們可以使用鎖的方式,來人為的制造“原子”,比如這里用互斥。

  

#include <stdio.h>
#include <pthread.h>

volatile int cnt = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* mythread(void* arg)
{
        int i;
        for(i=0;i<500000000;i++) {
                pthread_mutex_lock(&mutex);
                cnt++;
                pthread_mutex_unlock(&mutex);
        }
        return NULL;
}

int main()
{
        pthread_t id, id2;

        pthread_create(&id, NULL, mythread, NULL);
        pthread_create(&id2, NULL, mythread, NULL);
        pthread_join(id, NULL);
        pthread_join(id2, NULL);
        printf("cnt = %d\n", cnt);

        return 0;
}

  測試一下

linux-p94b:/tmp/testhere # gcc -O2 test1.c -lpthread
linux-p94b:/tmp/testhere # for((i=0;i<10;i++));do ./a.out ; done
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM