線程同步:Mutex和Condition

Android提供了兩個封裝好的同步類,它們是Mutex和Condition。這是重量級的同步技術,一般內核都會有對應的支持。另外,OS還提供了簡單的原子操作,這些也算是同步技術中的一種。下面分別來介紹這三種東西。
1. 互斥類—Mutex
Mutex是互斥類,用於多線程訪問同一個資源的時候,保證一次只有一個線程能訪問該資源。在《Windows核心編程》①一書中,對於這種互斥訪問有一個很形象的比喻:想象你在飛機上如廁,這時衛生間的信息牌上顯示“有人”,你必須等裏面的人出來後纔可進去。這就是互斥的含義。
下面來看Mutex的實現方式,它們都很簡單。
(1)Mutex介紹
其代碼如下所示:
[-->Thread.h::Mutex的聲明和實現]
inline Mutex::Mutex(int type, const char* name) {
    if (type == SHARED) {
       //type如果是SHARED,則表明這個Mutex支持跨進程的線程同步。
      //以後我們在Audio系統和Surface系統中會經常見到這種用法。
        pthread_mutexattr_t attr;
        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);
    }
}
inline Mutex::~Mutex() {
    pthread_mutex_destroy(&mMutex);
}
inline status_t Mutex::lock() {
    return -pthread_mutex_lock(&mMutex);
}
inline void Mutex::unlock() {
    pthread_mutex_unlock(&mMutex);
}
inline status_t Mutex::tryLock() {
    return -pthread_mutex_trylock(&mMutex);
}
關於Mutex的使用,除了初始化外,最重要的是lock和unlock函數的使用,它們的用法如下:
 要想獨佔衛生間,必須先調用Mutex的lock函數。這樣,這個區域就被鎖住了。如果這塊區域之前已被別人鎖住,lock函數則會等待,直到可以進入這塊區域爲止。系統保證一次只有一個線程能lock成功。
 當你“方便”完畢,記得調用Mutex的unlock以釋放互斥區域。這樣,其他人的lock纔可以成功返回。
 另外,Mutex還提供了一個trylock函數,該函數只是嘗試去鎖住該區域,使用者需要根據trylock的返回值來判斷是否成功鎖住了該區域。
注意 以上這些內容都和Raw API有關,不瞭解它的讀者可自行學習相關知識。在Android系統中,多線程也是常見和重要的編程手段,務必請大家重視。
Mutex類確實比Raw API方便好用,不過還是稍顯麻煩。
(2)AutoLock介紹
AutoLock類是定義在Mutex內部的一個類,它其實是一幫“懶人”搞出來的,爲什麼這麼說呢?先來看看使用Mutex有多麻煩:
 顯示調用Mutex的lock。
 在某個時候記住要調用該Mutex的unlock。
以上這些操作都必須一一對應,否則會出現“死鎖”!在有些代碼中,如果判斷分支特別多,你會發現unlock這句代碼被寫得比比皆是,如果稍有不慎,在某處就會忘了寫它。有什麼好辦法能解決這個問題嗎?終於有人想出來一個好辦法,就是充分利用了C++的構造和析構函數,只需看一看AutoLock的定義就會明白。代碼如下所示: 
[-->Thread.h Mutex::Autolock聲明和實現]
    class Autolock {
    public:
        //構造的時候調用lock。
        inline Autolock(Mutex& mutex) : mLock(mutex)  { mLock.lock(); }
        inline Autolock(Mutex* mutex) : mLock(*mutex) { mLock.lock(); }
        //析構的時候調用unlock。
        inline ~Autolock() { mLock.unlock(); }
    private:
        Mutex& mLock;
    };
AutoLock的用法很簡單:
 先定義一個Mutex,如 Mutex xlock。
 在使用xlock的地方,定義一個AutoLock,如 AutoLock autoLock(xlock)。
由於C++對象的構造和析構函數都是自動被調用的,所以在AutoLock的生命週期內,xlock的lock和unlock也就自動被調用了,這樣就省去了重複書寫unlock的麻煩,而且lock和unlock的調用肯定是一一對應的,這樣就絕對不會出錯。
2. 條件類—Condition
多線程同步中的條件類對應的是下面這種使用場景:
線程A做初始化工作,而其他線程比如線程B、C必須等到初始化工作完後才能工作,即線程B、C在等待一個條件,我們稱B、C爲等待者。
當線程A完成初始化工作時,會觸發這個條件,那麼等待者B、C就會被喚醒。觸發這個條件的A就是觸發者。
上面的使用場景非常形象,而且條件類提供的函數也非常形象,它的代碼如下所示:
[-->Thread.h:: Condition的聲明和實現]
class Condition {
public:
    enum {
        PRIVATE = 0,
        SHARED = 1
    };


    Condition();
    Condition(int type);//如果type是SHARED,表示支持跨進程的條件同步
    ~Condition();
    //線程B和C等待事件,wait這個名字是不是很形象呢?
    status_t wait(Mutex& mutex);
  //線程B和C的超時等待,B和C可以指定等待時間,當超過這個時間,條件卻還不滿足,則退出等待。
    status_t waitRelative(Mutex& mutex, nsecs_t reltime);
    //觸發者A用來通知條件已經滿足,但是B和C只有一個會被喚醒。
    void signal();
    //觸發者A用來通知條件已經滿足,所有等待者都會被喚醒。
    void broadcast();


private:
#if defined(HAVE_PTHREADS)
    pthread_cond_t mCond;
#else
    void*   mState;
#endif
}
聲明很簡單,定義也很簡單,代碼如下所示:
inline Condition::Condition() {
    pthread_cond_init(&mCond, NULL);
}
inline Condition::Condition(int type) {
    if (type == SHARED) {//設置跨進程的同步支持。
        pthread_condattr_t attr;
        pthread_condattr_init(&attr);
        pthread_condattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
        pthread_cond_init(&mCond, &attr);
        pthread_condattr_destroy(&attr);
    } else {
        pthread_cond_init(&mCond, NULL);
    }
}
inline Condition::~Condition() {
    pthread_cond_destroy(&mCond);
}
inline status_t Condition::wait(Mutex& mutex) {
    return -pthread_cond_wait(&mCond, &mutex.mMutex);
}
inline status_t Condition::waitRelative(Mutex& mutex, nsecs_t reltime) {
#if defined(HAVE_PTHREAD_COND_TIMEDWAIT_RELATIVE)
    struct timespec ts;
    ts.tv_sec  = reltime/1000000000;
    ts.tv_nsec = reltime%1000000000;
    return -pthread_cond_timedwait_relative_np(&mCond, &mutex.mMutex, &ts);
     ...... //有些系統沒有實現POSIX的相關函數,所以不同的系統需要調用不同的函數。
#endif 
}
inline void Condition::signal() {
    pthread_cond_signal(&mCond);
}
inline void Condition::broadcast() {
    pthread_cond_broadcast(&mCond);
}
可以看出,Condition的實現全是憑藉調用了Raw API的pthread_cond_xxx函數。這裏要重點說明的是,Condition類必須配合Mutex來使用。什麼意思?
在上面的代碼中,不論是wait、waitRelative、signal還是broadcast的調用,都放在一個Mutex的lock和unlock範圍中,尤其是wait和waitRelative函數的調用,這是強制性的。
來看一個實際的例子,加深一下對Condition類和Mutex類的印象。這個例子是Thread類的requestExitAndWait,目的是等待工作線程退出,代碼如下所示:
[-->Thread.cpp]
status_t Thread::requestExitAndWait()
{
    ......
   requestExit(); //設置退出變量mExitPending爲true。
    Mutex::Autolock _l(mLock);//使用Autolock,mLock被鎖住。
    while (mRunning == true) {
    /*
     條件變量的等待,這裏爲什麼要通過while循環來反覆檢測mRunning?
     因爲某些時候即使條件類沒有被觸發,wait也會返回。關於這個問題,強烈建議讀者閱讀
     前面推薦的《Programming with POSIX Thread》一書。
   */
      mThreadExitedCondition.wait(mLock); 
    }


    mExitPending = false;
   //退出前,局部變量Mutex::Autolock _l的析構會被調用,unlock也就會被自動調用。
    return mStatus; 
}
那麼,什麼時候會觸發這個條件呢?是在工作線程退出前。其代碼如下所示:
[-->Thread.cpp]
int Thread::_threadLoop(void* user)
{
    Thread* const self = static_cast<Thread*>(user);
    sp<Thread> strong(self->mHoldSelf);
    wp<Thread> weak(strong);
    self->mHoldSelf.clear();


    do {
          ......  
          result = self->threadLoop();//調用子類的threadLoop函數。
           ......
         //如果mExitPending爲true,則退出。
        if (result == false || self->mExitPending) {
            self->mExitPending = true;
            //退出前觸發條件變量,喚醒等待者。
            self->mLock.lock();//lock鎖住。
            //mRunning的修改位於鎖的保護中。如果你閱讀了前面推薦的書,這裏也就不難理解了。
            self->mRunning = false; 
            self->mThreadExitedCondition.broadcast();
            self->mLock.unlock();//釋放鎖。
            break;//退出循環,此後該線程函數會退出。
        }
        ......
    } while(strong != 0);
    
    return 0;
}
關於Android多線程的同步類,暫時介紹到此吧。當然,這些類背後所隱含的知識及技術是讀者需要倍加重視的。

提示 希望我們能養成一種由點及面的學習方法。以我們的同步類爲例,假設你是第一次接觸多線程編程,也學會了如何使用Mutex和Condition這兩個類,不妨以這兩個類代碼中所傳遞的知識作爲切入點,把和多線程相關的所有知識(這個知識不僅僅是函數的使用,還包括多線程的原理,多線程的編程模型,甚至是現在很熱門的並行多核編程)普遍瞭解一下。只有深刻理解並掌握了原理等基礎和框架性的知識後,才能以不變應萬變,才能做到遊刃有餘。



轉自:http://book.51cto.com


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章