你好,游客 登录 注册 搜索
背景:
阅读新闻

Android进程线程之同步互斥(一) - LXS的专栏

[日期:2013-04-18] 来源:  作者: [字体: ]

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。






收藏 推荐 打印 | 录入:admin | 阅读:
相关新闻