多執行緒程式設計之資料訪問互斥——原理性
在多執行緒存在的環境中,除了堆疊中的臨時資料之外,所有的資料都是共享的。如果我們需要執行緒之間正確地執行,那麼務必需要保證公共資料的執行和計算是正確的。簡單一點說,就是保證資料在執行的時候必須是互斥的。否則,如果兩個或者多個執行緒在同一時刻對資料進行了操作,那麼後果是不可想象的。
保證多執行緒之間的資料訪問互斥,有以下四類方法:
(1)關中斷
(2)數學互斥方法
(3)作業系統提供的互斥方法
(4)CPU原子操作
下面針對這四種方法進行詳細說明:
(1)關中斷
既然多執行緒之間的中斷切換會導致訪問同一資料的不同步,那麼關閉執行緒中斷切換肯定能過避免這個問題。而且,Intel X86系列CPU中確實存在這樣的關閉中斷指令。參照如下程式碼:
#include <stdio.h> int main() { __asm{ cli sti } return 1; }
其中cli是關中斷,sti是開中斷。這段程式碼沒有什麼問題,可以編過,當然也可以生成執行檔案。但是在執行的時候會出現一個異常告警:Unhandled exception in test.exe: 0xC0000096: Privileged Instruction。告警已經說的很清楚了,這是一個特權指令。只有系統或者核心本身才可以使用這個指令。
不過,大家也可以想象一下。因為平常我們編寫的程式都是應用級別的程式,要是每個程式都是用這些程式碼,那不亂了套了。比如說,你不小心安裝一個低質量的軟體,說不定什麼時候把你的中斷關了,這樣你的網路就斷了,你的輸入就沒有迴應了,你的音樂什麼都沒有了,這樣的環境你受的了嗎?應用層的軟體是千差萬別的,軟體的水平也是參差不齊的,所以系統不可能相信任何一個私有軟體,它相信的只是它自己。簡單來說,作為應用程式開發,這個方法肯定是不可取的。
(2)資料方法
通過某個數學演算法,可以確保不同的執行緒之間只可能其中一個訪問某個資料。例如有兩個執行緒操作同一個變數,可以採用如下演算法:
unsigned int flag[2] = {0}; unsigned int turn = 0; void process(unsigned int index) { flag[index] = 1; turn = 1 - index; while(flag[1 - index] && (turn == (1 - index))); do_something(); flag[index]= 0; }
其實,學過作業系統的朋友都知道,上面的演算法其實就是Peterson演算法,可惜它只能用於兩個執行緒的資料互斥。當然,這個演算法還可以推廣到更多執行緒之間的互斥,那就是bakery演算法。但是數學演算法有兩個缺點:
a)佔有空間多,兩個執行緒就要flag佔兩個單位空間,那麼n個執行緒就要n個flag空間;
b)程式碼編寫複雜,考慮的情況比較複雜。
(3)作業系統提供的互斥方法
系統提供的互斥演算法其實是我們平時開發中用的最多的互斥工具。就拿windows來說,關於互斥的工具就有臨界區、互斥量、訊號量等等。這類演算法有一個特點,那就是都是依據系統提高的互斥資源,那麼系統又是怎麼完成這些功能的呢?其實也不難。
舉一個最簡單的系統鎖實現方法:
void Lock(HANDLE hLock) { __asm {cli}; while(1){ if(/* 鎖可用*/){ /* 設定標誌,表明當前鎖已被佔用 */ __asm {sti}; return; } __asm{sti}; schedule(); __asm{cli}; } } void UnLock(HANDLE hLock) { __asm {cli}; /* 設定標誌, 當前鎖可用 */ __asm{sti}; }
從程式碼中可以看出,採用的CPU的中斷關閉與開啟指令就能夠實現一個簡單的系統鎖。不過這個例子沒有考慮就緒執行緒的壓棧等問題,實際情況會更加複雜些。
(4)CPU原子操作
在多執行緒中經常會涉及到一個經常用到而又非常簡單的計算操作,這個時候使用互斥量、訊號量等實現互斥操作顯得不划算。因此,CPU廠商將一些常用的操作設計成原子指令,在Windows系統中也稱之為原子鎖。常用的原子操作包括:
InterLockedAdd
InterLockedExchange
InterLockedCompareExchange
InterLockedIncrement
InterLockedDecrement
InterLockedAnd
InterLockedOr