轻松理解多线程同步基础概念

现在在移动平台(iOS,android)上编程使用到多线程是很常见的事情了。多线程编程的关键在于处理好各线程间多资源占用的同步处理。涉及到的概念有semaphore,mutex,lock,condition等等,这里简单的介绍下这几个关键的概念。

Semaphore(信号量)

Semaphore是多线程编程时一个很重要的概念,概念本身并不复杂,但要做到正确使用却不容易。这里我们从面向对象的角度来理解这个概念。现实生活中的任何对象或实体,我们都可以用class来描述它。Semaphore也不例外,如果用class定义应该是这样:

Semaphore对象里包含一个count值和一个队列对象,另外有两个对外public的方法,wait()和signal(),需要特别注意的事,count值代表的资源数量是不能为负的。为了理解这些属性和方法,我们可以类比一个现实生活中的例子。大家去餐厅吃饭,假设这个餐厅有10个座位,有20个吃货随机出发去这个餐厅吃饭,那么对应关系是这样的:

  • count = 10,10个座位。
  • queue,餐厅位置有限,为了避免混乱,餐厅肯定会吃货们排队。
  • wait(),吃货到了餐厅找服务员要位置点餐,这个行为就是wait。
  • signal(),吃货吃完了买单离开位置,这个行为就是signal。

这其实是一个信号量应用的典型场景,这里关键在于正确理解wait和signal发生时都有哪些细节步骤。用代码来描述大概是这样:

wait

具体到餐厅到例子,20个人随机出发去餐厅吃饭,有10个人先到,然后挨个执行wait。前10个人执行wait的时候是有位置的,所以count>0,这10个人每人都消耗掉一个座位开始吃饭。到第11个人到了都时候,count==0,没有位置了,所以被suspend,开始加入排队都队列等待。后续所有人都慢慢的到来,但和第11个人一样,都只能排队。

signal

过了一段时间之后,有个人吃好结账离开了餐厅。这时候如果没有人在排队,位置数量count ++,没有其它事情发生。但如果有人在排队,比如上面的情况,有10个人在等待位置,餐厅会把排在第一个的人安排到刚才空出来的位置,count值没有变化,但队列的人少了一个。

特别注意

对于wait和signal还有两点需要特别注意。也是平时我们使用semaphore时比较容易产生bug的地方。

  • wait和signal都是原子操作。可以简单理解为上面代码里wait(),signal()两个函数都是加锁的。这个特性其实让semaphore的行为变得更简单清晰。大家想象,如果到餐厅的10个人是同时到达的,但不是依次询问餐厅是否有位置,而是10张嘴同时说话,同时找餐厅要位置,显然情况会变得复杂不好处理。
  • wait或者signal调用的顺序是不确定的。上面的例子中每个人都是随机时间出发,到达餐厅的顺序也是随机的,并不一定先出发的就先到。同理每个人吃饭的时间长短也不一定,有人快有人慢,所以吃好离开餐厅的时间点也是随机的。这里每个人都代表一个线程,因为操作系统线程调度策略导致到底哪个线程先执行也是不确定的。

Mutex(互斥量)

理解了Semaphore,再看Mutex就很简单了。可以把Mutex理解成count == 1的Semaphore。在使用Mutex的场景下,永远都只允许有一个线程在占有资源,其它的线程都必须等待。建议大家按照count=1把上面Semaphore的例子再在脑子里过一遍,加深理解。

Lock(锁)

上面说的Semaphore和Mutex都是操作系统层面的基础概念。但具体到某个平台的时候,平台会对这两个概念再做一次封装以方便使用。比如在iOS上就有NSLock这个类,提供lock(),unlock()两个功能。但其实Lock在概念上和Mutex是一致的。当然平台既然做了封装就会提供额外的功能或者做一些额外的处理。这也是为什么在iOS里,NSLock的性能会比pthread_mutex会差一些。iOS里各种锁性能对比可以参考这篇文章

Condition(条件变量)

另一个遇到机会相对较少的概念是Condition,Condition理解起来很容易和上面几个概念混淆,但是只要和Semaphore对比下不同之处理解就很简单了。Condition甚至可以理解成一种“特殊”的Semaphore。特殊之处就在于它没有count(资源数)。它也有wait和signal两种行为。用代码简单表示是这样的:

逻辑其实变更简单了,可以从事件的角度去看待Condition。每次condition调用wait的时候,表示它想等待某个事件的发生(不管之前有没有发生过),所以一定是加入到等待队列当中。调用signal的时候,表示这个事件发生了,如果有线程在队列里等待,则取出其中一个来执行,后面的继续等待后续事件。如果队列是空的,这个事件就丢了,当什么也没有发生过。所以这里的关键在于对count(资源)和事件的理解。

具体平台实现condition概念的时候也会又一些调整。比如iOS里的NSCondition,它其实是Lock和Condition的集合体。即提供Lock的lock() unlock()方法,同时提供Condition的wait() signal()。