1.1.1 Android中的同步與互斥
Android系統也提供了自己的同步互斥機制,不過任何技術的本質都是類似的,更多的是把這些本質的東西應用到符合自己要求的情境。目前Android封裝的同步互斥類包括:
· Mutex
標頭檔在frameworks/native/include/utils/Mutex.h,因為實現與具體的平台有關,我們只關心如何使用它
· Condition
標頭檔在frameworks/native/include/utils/Condition.h,同樣我們只是應用它
· Barrier
frameworks/native/services/surfaceflinger/Barrier.h
這是基於Mutex和Condition實現的一個模型,目前只被用於SurfaceFlinger中,我們在後續的章節會碰到
1.1.1.1 Mutex
Mutex類中有一個enum定義,如下:
class Mutex {
public:
enum {
PRIVATE = 0,
SHARED = 1
};
如果是SHARED的話,說明它是適用於跨進程共用的。比如AudioTrack與AudioFlinger就駐留在兩個不同的進程,所以它們的mutex就是這種類型的:
/*frameworks/av/media/libmedia/AudioTrack.cpp*/
audio_track_cblk_t::audio_track_cblk_t()
: lock(Mutex::SHARED), cv(Condition::SHARED), user(0),server(0),
userBase(0),serverBase(0), buffers(NULL), frameCount(0),
loopStart(UINT_MAX),loopEnd(UINT_MAX), loopCount(0), mVolumeLR(0x10001000),
mSendLevel(0), flags(0)
{
}
Mutex類中有三個重要的成員函數:
status_t lock(); //擷取資源鎖
void unlock();//釋放資源鎖
status_t tryLock(); /*如果當前資源可用就lock,否則也直接返回,傳回值0代表成功。可見它和lock()
的區別在於不論成功與否都會及時返回,而不是等待*/
它的建構函式有三個:
/*frameworks/native/include/utils/Mutex.h*/
inline Mutex::Mutex() {
pthread_mutex_init(&mMutex, NULL);
}
inline Mutex::Mutex(const char* name) {
pthread_mutex_init(&mMutex, NULL);
}
inline Mutex::Mutex(int type, const char* name) {
if (type == SHARED) {
pthread_mutexattr_tattr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
pthread_mutex_init(&mMutex, &attr);
pthread_mutexattr_destroy(&attr);
} else {
pthread_mutex_init(&mMutex, NULL);
}
}
可見Mutex實際上也是基於pthread的封裝。
1.1.1.2 Condition
Condition表達的意思是“條件”,換句話說,它的核心是“條件是否滿足”——滿足的話執行某操作,不滿足的話則進入等待,直到條件滿足有人喚醒它。
有人可能會問,這種情況用Mutex能實現嗎?從理論上講,的確是可以。舉個例子來說,假設兩個線程A和B共用一個全域變數vari,它們的行為如下:
Thread A: 不斷去修改vari,每次改變後的值未知
Thread B: 當vari為0時,它需要做某些動作
也就是說,線程A是想獲得vari的訪問權,而線程B等待的是vari==0的情況。那麼如果用Mutex去完成的話,線程B就只能通過不斷地讀取vari來判斷條件是否滿足,有點類似於下面的虛擬碼:
while(1)
{
acquire_mutex_lock();//擷取對vari的Mutex鎖
if(0 == vari) //條件滿足
{
release_mutex_lock();//釋放鎖
break;
}
else
{
release_mutex_lock();//釋放鎖
sleep();//休眠一段時間
}
}
那麼對於線程B而言,它什麼時候達到條件(vari==0)是未知的,這點和其它共用vari的線程(比如線程A)有很大不同,因而採用輪詢的方式顯然極大的浪費了CPU時間。
再舉一個生活中的例子,加深大家的理解。比如有一個公用廁所,假設同時只能供一個人使用。現在想使用這一資源的有兩類人:其一當然是正常使用廁所的人;其二就是更換廁紙的人員。如果我們把他們一視同仁的話會發生什麼情況呢?那就是工作人員也要排隊。等排到他時,他進去看下廁所紙是否用完,有的話就更換,否則就什麼也不做直接退出,然後繼續排隊等待,如此迴圈往複。換句話說,這位工作人員的效率是相當低的,因為他的時間都浪費在排隊上了。
所以我們需要尋找另一種模型來解決這一特殊的情境。解決方案之一就是工作人員不需要排隊,而是由其他人通知他廁所缺紙的事件。這樣子既減少了排隊人員的數量,同時提高了工作人員的效率,一舉兩得。
Condition就是針對這些情境提出的解決方案。
class Condition {
public:
enum { //和Mutex一樣,它支援跨進程共用
PRIVATE = 0,
SHARED = 1
};
…
status_t wait(Mutex& mutex); //在某個條件上等待
status_t waitRelative(Mutex& mutex, nsecs_treltime); //也是在某個條件上等待,增加了逾時退出功能
void signal(); //條件滿足時通知相應等待者
void broadcast(); //條件滿足時通知所有等待者
private:
#if defined(HAVE_PTHREADS)
pthread_cond_t mCond;
#else
void* mState;
#endif
};
從Condition提供的幾個介面函數中,我們有如下疑問:
· 既然wait()是在等待“條件滿足”,那麼是什麼樣的條件呢?
在整個Condition類的描述中,我們都看不到與條件相關的變數或者操作。這是因為,Condition實際上是一個“半成品”,它並不提供具體的“條件”——理由很簡單,在不同情況下,使用者所需的“條件”形式都是不一樣的,Condition想要提供一種“通用的解決方案”,而不是針對某些具體的“條件樣式”去設計。比如我們可以說“滿足條件”就是某變數A為True,或者是變數A達到值100, 或者是變數A等於B,等等。這是Condition所無法預料的,因而它能做的,就是提供一個“黑盒”,而不管盒子裡的是什麼
· 為什麼需要mutex?
相信大家都注意到了,wait和waitRelative介面都帶有一個Mutex&mutex變數,這是很多人感到不解的地方——既然都有Condition這一互斥方法了,為什麼還要牽扯一個Mutex呢?
由於Condition本身的不完整性,如果直接從理論分析的話估計不好理解,所以我們希望結合下一小節的Barrier來給大家解答上述兩個問題。
1.1.1.3 Barrier
Condition表示“條件”,而Barrier表示“柵欄、障礙”。後者是對前者的一個應用,換句話說,Barrier是填充了“具體條件”的Condition,這給我們理解Condition提供了一個很好的執行個體。
Barrier是定義在SurfaceFlinger這一塊的,並不是像Condition一樣作為常用Utility提供給整個Android系統使用。不過這不影響我們對它的分析。
/*frameworks/native/services/surfaceflinger/Barrier.h*/
class Barrier
{
public:
inline Barrier() :state(CLOSED) { }
inline ~Barrier() { }
void open() {
Mutex::Autolock_l(lock);
state = OPENED;
cv.broadcast();
}
void close() {
Mutex::Autolock_l(lock);
state = CLOSED;
}
void wait() const {
Mutex::Autolock_l(lock);
while (state ==CLOSED) {
cv.wait(lock);
}
}
private:
enum { OPENED, CLOSED };
mutable Mutex lock;
mutable Condition cv;
volatile int state;
};
Barrier總共提供了三個介面函數,即wait()、open()和close()。我們說它是Condition的執行個體,那麼“條件”是什麼呢?稍微觀察一下就能發現,是其中的變數state==OPENED,另一個狀態當然就是CLOSED——這有點類似於汽車柵欄的開啟和關閉。在汽車通過前,它必須要先確認柵欄是開啟的,於是調用wait(),如果條件不滿足那麼汽車就只能停下來等待。這個函數首先擷取一個Mutex鎖,然後才是調用Condition對象cv,為什麼呢?我們知道Mutex是用於線程間共用互斥資源的,這說明wait()中接下來的操作涉及到了對某一互斥資源的訪問。這一資源很明顯的就是state這個變數。可以想象一下假如沒有一把對state訪問的鎖,那麼當wait與open/close同時去操作它時,有沒有可能引起問題呢?
假設有如下步驟:
Step 1. wait()取得state值,發現是CLOSED
Step 2. open()取得state值,將其改為OPENED
Step 3. open()喚醒正在等待的線程。因為此時wait()還沒有進入睡眠,所以實際上沒有線程需要喚醒
Step4.wait()因為state==CLOSED,所以進入等待,但這時候的柵欄卻已經是開啟的了,這將導致wait()調用者所線上程得不到喚醒
這樣子就很清楚了,對於state的訪問必須有一個互斥鎖的保護。
先來看下Condition::wait()的實現:
inline status_t Condition::wait(Mutex& mutex) {
return-pthread_cond_wait(&mCond, &mutex.mMutex);
}
很簡單,直接調用了pthread中的方法。
pthread_cond_wait的邏輯語義如下:
1. 釋放鎖mutex
2. 進入休眠等待
3. 喚醒後再擷取mutex鎖
這裡經曆了先釋放再擷取鎖的步驟,什麼原因?
由於wait即將進入休眠等待,假如此時它不先釋放Mutex鎖,那麼open()/close()又如何能訪問“條件變數”state呢?這無疑會使程式陷入互相等待的死結狀態。所以它需要先行釋放鎖,再進入睡眠。之後因為open()操作完畢會釋放鎖,也就讓wait()有機會再次獲得這一Mutex。
同時我們注意到,判斷條件是否滿足的語句是一個while迴圈:
while (state == CLOSED) {…
這樣做也是合理的。可以假設一下,如果我們在close()的末尾也加一個broadcast()或者signal(),那麼wait()同樣會被喚醒,但是條件滿足了嗎?顯然沒有,所以wait()只能再次進入等待,直到條件真正為OPENED為止。
值得注意的是,wait()函數的結尾會自動釋放Mutex lock(Autolock的描述見下一小節),也就是說wait()返回時,程式已經不再擁有對共用資源的鎖了。個人認為如果接下來的代碼還依賴於對共用資源的操作,那麼就應該再次擷取鎖,否則還是會出錯。舉個上面的例子來說,當wait()返回時,我們的確可以認為此時汽車柵欄是已經開啟的。但是因為釋放了鎖,很有可能在汽車發動的過程中,又有人把它關閉了。這導致的後果就是汽車會直接撞上柵欄引起事故。Barrier通常被用於對某線程是否初始化完成的判斷上,這種情境具有無法復原性——既然已經初始化了,那麼後期就不可能再出現“沒有初始化”的情況了,因而即便wait()返回後沒有擷取鎖也被認為是安全的。
條件變數Condition是和互斥鎖Mutex同樣重要的一種資源保護手段,大家一定要把它們都理解清楚。當然,我們更多的是從使用的角度去學習,至於pthread_cond_wait是如何?的,涉及到具體的硬體平台,可以不用去深究。
1.1.1.4 Autolock
在Mutex類內部還有一個Autolock嵌套類,從字面上看它應該是為了實現自動地加解鎖操作,那麼如何?呢?
其實很簡單,看下這個類的構造和解構函式大家就明白了:
class Autolock {
public:
inlineAutolock(Mutex& mutex) : mLock(mutex) { mLock.lock(); }
inline Autolock(Mutex*mutex) : mLock(*mutex) { mLock.lock(); }
inline ~Autolock() {mLock.unlock(); }
private:
Mutex& mLock;
};
也就是說,當Autolock構造時,主動調用內部成員變數mLock的lock()方法,而在析構時正好相反,調用它的unlock()方法釋放鎖。這樣的話,假如一個Autolock對象是局部變數,則在生命週期結束時就自動的把資源鎖解了。舉個AudioTrack中的例子,如下所示:
/*frameworks/av/media/libmedia/AudioTrack.cpp*/
uint32_t audio_track_cblk_t::framesAvailable()
{
Mutex::Autolock _l(lock);
returnframesAvailable_l();
}
變數_l就是一個Autolock對象,它在構造時會主動調用audio_track_cblk_t 中的lock鎖,而當framesAvailable()結束時,_l的生命週期也隨之完結,於是lock所對應的鎖也會被開啟。這是一個實現上的小技巧,在某些情況下可以有效防止開發人員沒有配套使用lock/unlock。