Android進程線程之同步互斥(一)

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。

 

 

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。