1. 程式人生 > >C語言/原子/編譯,你真的明白了嗎?

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

clas done ati pre 內存 程序 導致 裏的 creat

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

#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

  

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