Linux環境多執行緒程式設計基礎設施
本文介紹多執行緒環境下並行程式設計的基礎設施。主要包括:
- Volatile
- __thread
- Memory Barrier
- __sync_synchronize
volatile
編譯器有時候為了優化效能,會將一些變數的值快取到暫存器中,因此如果編譯器發現該變數的值沒有改變的話,將從暫存器裡讀出該值,這樣可以避免記憶體訪問。
但是這種做法有時候會有問題。如果該變數確實(以某種很難檢測的方式)被修改呢?那豈不是讀到錯的值?是的。在多執行緒情況下,問題更為突出:當某個執行緒對一個記憶體單元進行修改後,其他執行緒如果從暫存器裡讀取該變數可能讀到老值,未更新的值,錯誤的值,不新鮮的值。
如何防止這樣錯誤的“優化”?方法就是給變數加上volatile修飾。
volatile int i=10;//用volatile修飾變數i ......//something happened int 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<iostream> #include<pthread.h> #include<unistd.h> using 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) { cout<<++i<<endl;//輸出 2 return NULL; } void* thread2(void* arg) { sleep(1); //等待thread1完成更新 cout<<++i<<endl;//輸出 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的值暫存入%eax movl $10, b(%rip) ; b = 10 addl $3, %eax ; %eax加3 movl %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。
相關文章
- 多執行緒程式設計基礎(一)-- 執行緒的使用執行緒程式設計
- Java-基礎程式設計-多執行緒Java程式設計執行緒
- 多執行緒程式設計基礎(二)-- 執行緒池的使用執行緒程式設計
- Linux C++ 多執行緒程式設計LinuxC++執行緒程式設計
- Java基礎之多執行緒程式設計Java執行緒程式設計
- Java多執行緒程式設計基礎知識彙總Java執行緒程式設計
- Java 多執行緒設計模式之基礎概念Java執行緒設計模式
- 併發程式設計之多執行緒基礎程式設計執行緒
- Java併發程式設計-執行緒基礎Java程式設計執行緒
- python 多執行緒程式設計Python執行緒程式設計
- JavaScript多執行緒程式設計JavaScript執行緒程式設計
- Python多執行緒程式設計Python執行緒程式設計
- NOI Linux 基礎知識與程式設計環境Linux程式設計
- Linux多執行緒伺服器端程式設計Linux執行緒伺服器程式設計
- 多執行緒程式設計ExecutorService用法執行緒程式設計
- 29. 多執行緒程式設計執行緒程式設計
- googleoppiaPythonWeb程式設計執行環境搭建GoPythonWeb程式設計
- JAVA SE 實戰篇 C1 多執行緒程式設計基礎Java執行緒程式設計
- Linux C/C++程式設計中的多執行緒程式設計基本概念LinuxC++程式設計執行緒
- [短文速讀 -5] 多執行緒程式設計引子:程式、執行緒、執行緒安全執行緒程式設計
- 【Linux網路程式設計-5】多執行緒服務端Linux程式設計執行緒服務端
- 深入理解多執行緒程式設計執行緒程式設計
- [02] 多執行緒邏輯程式設計執行緒程式設計
- iOS開發-多執行緒程式設計iOS執行緒程式設計
- Task+ConcurrentQueue多執行緒程式設計執行緒程式設計
- 多執行緒程式設計總結:一、認識多執行緒本質執行緒程式設計
- 多執行緒系列(1),多執行緒基礎執行緒
- C#多執行緒程式設計實戰1.1建立執行緒C#執行緒程式設計
- C#多執行緒程式設計-基元執行緒同步構造C#執行緒程式設計
- 多執行緒基礎執行緒
- Java多執行緒程式設計—鎖優化Java執行緒程式設計優化
- 多執行緒程式設計相關理論執行緒程式設計
- 多執行緒併發程式設計“鎖”事執行緒程式設計
- 使用Java實現多執行緒程式設計Java執行緒程式設計
- C語言 之 多執行緒程式設計C語言執行緒程式設計
- 多執行緒程式設計的核心思想執行緒程式設計
- 【linux】系統程式設計-5-執行緒Linux程式設計執行緒
- 多執行緒學習一(多執行緒基礎)執行緒
- 程式執行緒篇——程式執行緒基礎執行緒