聲明:本文為個人了解,不能保證一定是正确的!
這三個概念一直糾纏着我,我也時不時的會拿出來辨析下,直到昨天才發現自己可以把它們理順了。是以學習就是這樣一個反複的過程,最終達到頓悟的效果。本文主要參考APUE第三版英文版第10.6和12.5節,以及WIKI百科,還有CSDN和stackoverflow中對這些概念的讨論,然後給出一份自己認為比較合理的了解。
中斷,信号,線程切換
==========================
- 這三個概念都牽涉到異步通信,因為運作中的代碼不可預測什麼時候會發生中斷,什麼時候會收到信号,什麼時候會發生線程切換:
- 中斷,一般指的硬體中斷,是硬體對cpu的中斷
- 信号,則是對中斷的模拟,可以看作是os對程序的中斷
- 線程切換,cpu的時分複用手段
- 代碼執行流:
#-表示代碼在cpu上運作,.表示等待中
##1. 中斷和信号
<f1>---------...................-----------
<f2>.........-------------------...........
###f1在執行的過程中被f2打斷,當且僅當f2完整的執行完後傳回f1
##2. 線程切換
<f1>----....----....----....----....----
<f2>....----....----....----....----....
###f1和f2互相打斷彼此,交錯地運作
可重入,異步資訊安全,線程安全
====================================
-
可重入
可重入的意思就是一個函數沒有執行完,又在另一個地方被調用一次,兩次調用都能得到正确的結果。可重入概念在多任務作業系統之前就已經存在了,It is a concept from the time when no multitasking operating systems existed。
-
異步資訊安全,線程安全
可重入中提到的”另一個地方”可以是:
-
在中斷或信号中,在此情況下如果函數是可重入的,那麼就稱這個函數是異步信号安全的,這兩種情況可以看成是一種遞歸調用。
[APUE10.6節就是專門說明異步信号安全的]
-
在另一個線程中,在此情況下如果函數是可重入的,那麼就稱這個函數是線程安全的。
[APUE12.5節就是專門說明線程安全的]
If a function is reentrant with respect to multiple threads, we say that it is thread-safe. This doesn’t tell us, however, whether the function is reentrant with respect to signal handlers. We say that a function that is safe to be reentered from an asynchronous signal handler is async-signal safe.
-
3. 三者的關系
紅色表示不可重入函數,A+B+C表示可重入函數,其中A表示遞歸調用情況下可重入,C表示多線程的情況下可重入,B表示種情況下都可重入。
是以單獨說某函數是重入,而不限定使用場景是沒有意義的;如果僅僅說是非可重入則又是有意義的…
4. 異步信号安全函數清單
5. 可重入函數和異步信号安全函數等同嗎?
根據上面的關系圖可以得出兩者是不等同的,可重入函數在不同的情景下可以分為異步信号安全和線程安全。異步信号安全可以看作是遞歸調用情況下的可重入。舉例如下:
//非可重入版本
int t;
void swap(int *x, int *y)
{
t = *x;
*x = *y;
// hardware interrupt might invoke isr() here!!
*y = t;
}
/
//異步信号安全版本,非線程安全
int t;
void swap(int *x, int *y)
{
int s;
s = t; // save global variable
t = *x;
*x = *y;
// hardware interrupt might invoke isr() here!
*y = t;
t = s; // restore global variable
}
//在信号處理函數中調用時,每次使用t之前都會進行備份,傳回之前還原t的值;是以在信号處理函數中調用是可重入的;
//如果用線上程中,則有可能線程切換之前未來得及還原t的值,導緻結果出錯,是以線上程切換中是不可重入的;
//由此可以看出,異步信号安全函數不一定是線程安全函數!
//線程安全的函數由于可能使用互斥鎖,在信号處理函數中遞歸調用會出現死鎖,是以線程安全的函數也不一定是異步信号安全的。
其它
=====
-
信号就像硬體中斷一樣,會打斷正在執行的指令序列。信号處理函數無法判斷捕獲到信号的時候,程序在何處運作。如果信号處理函數中的操作與打斷的函數的操作相同,而且這個操作中有靜态資料結構等,當信号處理函數傳回的時候(當然這裡讨論的是信号處理函數可以傳回),恢複原先的執行序列,可能會導緻信号處理函數中的操作覆寫了之前正常操作中的資料。
是以通常函數不可重入的原因在于:
- 函數使用靜态資料結構;
- 函數調用malloc和free.因為malloc通常會為所配置設定的存儲區維護一個連結表,而插入執行信号處理函數的時候,程序可能正在修改此連結表;
- 函數是标準IO函數,因為标準IO庫的很多實作都使用了全局資料結構;
- 函數會修改自身代碼,導緻多次調用不同代碼;
- 等等。
-
即使對于可重入函數,在信号處理函數中使用也需要注意一個問題就是errno。一個線程中隻有一個errno變量,信号處理函數中使用的可重入函數也有可能會修改errno。例如,read函數是可重入的,但是它也有可能會修改errno。是以,正确的做法是在信号處理函數開始,先儲存errno;在信号處理函數退出的時候,再恢複errno。
例如,程式正在調用printf輸出,但是在調用printf時,出現了信号,對應的信号處理函數也有printf語句,就會導緻兩個printf的輸出混雜在一起。
如果是給printf加鎖的話,同樣是上面的情況就會導緻死鎖。對于這種情況,采用的方法一般是在特定的區域屏蔽一定的信号。
屏蔽信号的方法:
1> signal(SIGPIPE, SIG_IGN); //忽略一些信号
2> sigprocmask()
sigprocmask隻為單線程定義的
3> pthread_sigmask()
pthread_sigmasks可以在多線程中使用
-
很多函數并不是線程安全的,因為他們傳回的資料是存放在靜态的記憶體緩沖區中的。通過修改接口,由調用者自行提供緩沖區就可以使這些函數變為線程安全的。作業系統實作支援線程安全函數的時候,會對POSIX.1中的一些非線程安全的函數提供一些可替換的線程安全版本。
例如,gethostbyname()是線程不安全的,在Linux中提供了gethostbyname_r()的線程安全實作。
函數名字後面加上”_r”,以表明這個版本是可重入的(對于線程可重入,也就是說是線程安全的,但并不是說對于信号處理函數也是可重入的,或者是異步信号安全的)。
- 多線程程式中常見的疏忽性問題
- 将指針作為新線程的參數傳遞給調用方棧,我就犯過這樣的錯…
- 在沒有同步機制保護的情況下通路全局記憶體的共享可更改狀态。
- 兩個線程嘗試輪流擷取對同一對全局資源的權限時導緻死鎖。其中一個線程控制第一種資源,另一個線程控制第二種資源。其中一個線程放棄之前,任何一個線程都無法繼續操作。
- 嘗試重新擷取已持有的鎖(遞歸死鎖)。
- 在同步保護中建立隐藏的間隔。如果受保護的代碼段包含的函數釋放了同步機制,而又在傳回調用方之前重新擷取了該同步機制,則将在保護中出現此間隔。結果具有誤導性。對于調用方,表面上看全局資料已受到保護,而實際上未受到保護。
- 将UNIX 信号與線程混合時,使用sigwait(2) 模型來處理異步信号。
- 調用setjmp(3C) 和longjmp(3C),然後長時間跳躍,而不釋放互斥鎖。
- 從對*_cond_wait() 或*_cond_timedwait() 的調用中傳回後無法重新評估條件。
-
如果一個函數對多個線程來說是可重入的,則說這個函數是線程安全的,但這并不能說明對信号處理程式來說該函數也是可重入的。
如果函數對異步信号處理程式的重入是安全的,那麼就可以說函數是”異步-信号安全”的。
參考文檔
==============
1. APUE第三版原版10.6&12.5,去閱讀原版而不是中文版(有道在手,天下我有:P)
2. https://en.wikipedia.org/wiki/Reentrancy_(computing)
3. http://bbs.csdn.net/topics/310140183
4. http://stackoverflow.com/questions/9837343/difference-between-thread-safe-and-async-signal-safe
5. http://stackoverflow.com/questions/18198487/are-r-unix-calls-reentrant-async-signal-safe-thread-safe-or-both