Linux 環境多線程編程基礎設施

來源:互聯網
上載者:User
本文介紹多線程環境下並行編程的基礎設施。主要包括:

  • volatile

  • __thread

  • Memory Barrier

  • __sync_synchronize

  • volatile

編譯器有時候為了最佳化效能,會將一些變數的值緩衝到寄存器中,因此如果編譯器發現該變數的值沒有改變的話,將從寄存器裡讀出該值,這樣可以避免記憶體訪問。

但是這種做法有時候會有問題。如果該變數確實(以某種很難檢測的方式)被修改呢?那豈不是讀到錯的值?是的。在多線程情況下,問題更為突出:當某個線程對一個記憶體單元進行修改後,其他線程如果從寄存器裡讀取該變數可能讀到老值,未更新的值,錯誤的值,不新鮮的值。

如何防止這樣錯誤的“最佳化”?方法就是給變數加上volatile修飾。

volatile int i=10;//用volatile修飾變數i......//something happenedint b = i;//強制從記憶體中讀取即時的i的值

OK,畢竟volatile不是完美的,它也在某種程度上限制了最佳化。有時候是不是有這樣的需求:我要你立即即時讀取資料的時候,你就訪問記憶體,別最佳化;否則,你該最佳化還是最佳化你的。能做到嗎?

不加volatile修飾,那麼就做不到前面一點。加了volatile,後面這一方面就無從談起,怎麼辦?傷腦筋。

其實我們可以這樣:

int i = 2; //變數i還是不用加volatile修飾#define ACCESS_ONCE(x) (* (volatile typeof(x) *) &(x))

需要即時讀取i的值時候,就調用ACCESS_ONCE(i),否則直接使用i即可。

這個技巧,我是從《Is parallel programming hard?》上學到的。


聽起來都很好?然而險象環生:volatile常被誤用,很多人往往不知道或者忽略它的兩個特點:在C/C++語言裡,volatile不保證原子性;使用volatile不應該對它有任何Memory Barrier的期待。


第一點比較好理解,對於第二點,我們來看一個很經典的例子:

volatile int is_ready = 0;char message[123];void thread_A{  while(is_ready == 0)  {  }  //use message;}void thread_B{  strcpy(message,"everything seems ok");  is_ready = 1;}

線程B中,雖然is_ready有volatile修飾,但是這裡的volatile不提供任何Memory Barrier,因此12行和13行可能被亂序執行,is_ready = 1被執行,而message還未被正確設定,導致線程A讀到錯誤的值。

這意味著,在多線程中使用volatile需要非常謹慎、小心。

__thread

__thread是gcc內建的用於多線程編程的基礎設施。用__thread修飾的變數,每個線程都擁有一份實體,相互獨立,互不干擾。舉個例子:

#include#include#includeusing namespace std;__thread int i = 1;void* thread1(void* arg);void* thread2(void* arg);int main(){  pthread_t pthread1;  pthread_t pthread2;  pthread_create(&pthread1, NULL, thread1, NULL);  pthread_create(&pthread2, NULL, thread2, NULL);  pthread_join(pthread1, NULL);  pthread_join(pthread2, NULL);  return 0;}void* thread1(void* arg){  coutiendl;//輸出 2    return NULL;}void* thread2(void* arg){  sleep(1); //等待thread1完成更新  coutiendl;//輸出 2,而不是3  return NULL;}


需要注意的是:

1,__thread可以修飾全域變數、函數的靜態變數,但是無法修飾函數的局部變數。

2,被__thread修飾的變數只能在編譯期初始化,且只能通過常量運算式來初始化。

Memory Barrier

為了最佳化,現代編譯器和CPU可能會亂序執行指令。例如:

int a = 1;int b = 2;a = b + 3;b = 10;


CPU亂序執行後,第4行語句和第5行語句的執行順序可能變為先b=10然後再a=b+3

有些人可能會說,那結果不就不對了嗎?b為10,a為13?可是正確結果應該是a為5啊。

哦,這裡說的是語句的執行,對應的彙編指令不是簡單的mov b,10和mov b,a+3。

產生的彙編代碼可能是:

movl    b(%rip), %eax ; 將b的值暫存入%eaxmovl    $10, b(%rip) ; b = 10addl    $3, %eax ; %eax加3movl    %eax, a(%rip) ; 將%eax也就是b+3的值寫入a,即 a = b + 3


這並不奇怪,為了最佳化效能,有時候確實可以這麼做。但是在多線程並行編程中,有時候亂序就會出問題。

一個最典型的例子是用鎖保護臨界區。如果臨界區的代碼被拉到加鎖前或者釋放鎖之後執行,那麼將導致不明確的結果,往往讓人不開心的結果。

還有,比如隨意將讀資料和寫資料亂序,那麼本來是先讀後寫,變成先寫後讀就導致後面讀到了髒的資料。因此,Memory Barrier就是用來防止亂序執行的。具體說來,Memory Barrier包括三種:

1,acquire barrier。acquire barrier之後的指令不能也不會被拉到該acquire barrier之前執行。

2,release barrier。release barrier之前的指令不能也不會被拉到該release barrier之後執行。

3,full barrier。以上兩種的合集。

所以,很容易知道,加鎖,也就是lock對應acquire barrier;釋放鎖,也就是unlock對應release barrier。哦,那麼full barrier呢?

__sync_synchronize

__sync_synchronize就是一種full barrier。

以上就是Linux 環境多線程編程基礎設施的內容,更多相關內容請關注topic.alibabacloud.com(www.php.cn)!

  • 相關文章

    聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.