在編寫伺服器軟體時,為了提高程式的穩定性,需要考慮執行緒安全、可重入和訊號安全。
當多執行緒軟體執行時,作業系統隨時可能暫停乙個執行緒的執行,將cpu分配給另外乙個執行緒。考慮下面的執行流程。假設執行緒1和執行緒2被排程到同乙個cpu上執行,它們分別執行int_to_str(10)和int_to_str(20)。
const char* int_to_str(int value)
假設執行緒1首先執行int_to_str(10)
。在即將返回時,作業系統進行執行緒排程,執行緒2開始執行。執行緒2執行int_to_str(20)
,之後作業系統再次進行執行緒排程,執行緒1恢復執行。這時緩衝區buffer中的值已經變成了字串"20",而按照設計要求,int_to_str(10)
應當返回字串"10"。
發生這種情況,是因為執行緒自身無法感知執行緒排程,同時各個執行緒又共享同一位址空間。當執行緒恢復執行時,之前讀取的資料可能已經被其他執行緒修改了,但執行緒自身意識不到這種變化。因此就產生了一種安全要求:無論作業系統如何排程執行緒,乙個函式都可以按照設計要求正確執行。這就是執行緒安全要求。在多執行緒環境下,非執行緒安全的函式的返回結果是不可信的。
要編寫執行緒安全函式,需要做到:
除了執行緒排程,執行緒也可能因為訊號的發生而暫停。假設執行緒正在執行函式foo,這時程序收到訊號。中斷處理函式中呼叫了函式foo,這種情況就是函式重入。可重入問題和執行緒排程無關,在單執行緒環境下也會發生。假設有乙個日誌函式,為每條日誌分配唯一的id,並列印日誌。
void log(const char *message)void handle_signal(int signum)
在上面的**中,log_id++
實際上會編譯為3條指令
mov eax, [log_id]add eax, 1
mov [log_id], eax
假設乙個單執行緒軟體在即將執行add eax, 1
時收到訊號(比如sigint),程式開始執行訊號處理函式handle_signal
。這時就會產生兩條具有相同id的日誌。
編寫可重入函式的原則和編寫執行緒安全函式是類似的:
可重入函式和執行緒安全函式的區別在於,如果使用非本地物件,可重入函式必須使用可重入鎖進行保護。可重入鎖和不可重入鎖的不同在於,如果當前執行緒已經持有鎖,對可重入鎖可以再次加鎖。但對不可重入鎖,再次加鎖會導致死鎖。
reentrantlock rl;rl.lock();
rl.lock(); // ok
nonreentrantlock nrl;
nrl.lock();
nrl.lock(); // deadlock
如果在函式中使用不可重入鎖,程式可能陷入死鎖。
nonreentrantlock nrl;void foo()
void handle_signal(int signum)
在多執行緒環境下,如果乙個函式既是執行緒安全的,又是可重入的,那麼這個函式是否「足夠」安全了呢?前面的討論缺少乙個重要的場景,如果訊號被分派給其他執行緒處理會怎麼樣?仍然考慮上一節的最後乙個例子,把例子中的不可重入鎖替換為可重入鎖。考慮在單cpu上執行的乙個多執行緒軟體。假設執行緒1正在執行函式foo,剛剛獲得鎖。這時發生了執行緒排程,執行緒2開始執行。接著程序收到訊號,並分派給執行緒2處理。執行緒2執行handle_signal
函式,並等待鎖的釋放。這時程式是否會陷入死鎖,取決於作業系統排程執行緒的方式。如果在訊號處理函式返回之前,作業系統不進行執行緒排程,執行緒1無法執行,鎖無法釋放,就會導致死鎖。
在編寫非同步訊號安全函式需要滿足下列條件:
訊號可重入執行緒安全
訊號可重入執行緒安全 2009 08 28 16 54 之所以把這幾個概念放一起,是因為它們組合在一起容易出現一些莫名其妙的錯誤,而且一旦出現,還很難被發現。更糟糕的是它們的出現需要一定的時間,並不是非常容易重現的,而且需要了解的比較多才能更好的理解它們發生的原因。這裡要用例子闡述一下。訊號的是un...
可重入和執行緒安全
執行緒安全這個詞對我來說已經不是很陌生的了,但是遇到乙個叫做可重入函式的詞,它給我的感覺和執行緒安全是這麼的相近,但既然拿出來了,肯定是有區別的,下面就說說他們之間的區別和聯絡。要先解釋這兩個詞語才行。執行緒安全 似乎是在牛客網刷題的時候看到乙個正確的選項說的是,執行緒安全問題都是由全域性變數及靜態...
可重入和執行緒安全
綜觀整個文件,術語可重入和執行緒安全總是被用來標記類和函式,表明他們如何被用在多執行緒程式中。乙個執行緒安全的函式能被多個執行緒同時呼叫,甚至在這些呼叫使用共享資料時,因為所有對共享資料的引用是序列進行的。乙個可重入的函式也能被多個執行緒同時呼叫,但前提是每個呼叫只使用它們自己的資料。所以,乙個執行...