4.2 同步

  在“生产者—消费者”的练习中,大部分人选择了由调用者来加锁:作为生产者,往双向链表里插入数据时,先加

锁,接着插入数据,然后解锁;作为消费者,从双向链表里取数据时,先加锁,接着删除数据,然后解锁。这是合理的

,不过有点麻烦:每个调用者都要做这些动作,如果其中一个调用者忘记了解锁的步骤,就会造成死锁。而且调用者必须明确自己是在多线程下工作,这些代码放到单线程的环境中就不能使用了。

  在很多情况下,由实现者来加锁是比较好的选择,那样对调用者更为友好,可以避免出现一些不必要的错误。比如

像目前Linux下流行的DBUS,它是一套进程间通信框架,它同时拥有支持单线程和多线程的版本,但调用者不需要明确如

何加锁/解锁,也不需要连接不同的库或用宏来进行控制,单线程版本和多线程版本的不同只是在一个初始化函数上。

  这里我们请读者对前面实现的双向链表做点改进。

  (1) 支持多线程和单线程的版本。对于多线程版本,由实现者(在链表)加锁/解锁,对于单线程版本,其性能不受

影响(或很小)。

  (2) 区分单线程版本和多线程版本时,既不需要链接不同的库,也不需要用宏来进行控制,完全可以在运行时切换

  (3) 保持双向链表的通用性,不依赖于特定的平台。

  面对这个需求,一些初学者可能有点蒙了。以前在学校的时候,对于课本后面的练习,我总是信心百倍,原因很简

单,我确信这些练习不管它的出现方式有多么不同,但总是与前面学过的知识有关。记得《如何求解问题:现代启发式

方法》 中说过,正是这种练习的方式妨碍了我们提升解决问题的能力,在现实中解决问题时我们通常没有这么幸运。在

本书中,我之所以把练习放在前面,就是为了刺激读者去思考,希望读者在学习知识的同时学习解决问题的方法。

  这里我们应该怎么分析呢?要在双向链表里加锁,第一是要区分单线程和多线程,要链接同一个库,而且不能用宏

来控制。第二是不能依赖于特定平台,但锁本身恰恰是依赖于平台的。怎么办?很明显这两个需求都要求锁的实现可以

变化:单线程版本中,它什么都不做;多线程版本中,不同的平台有不同的实现。

  我们要做的就是隔离变化。应该怎样隔离变化?前面我们已经练习过几次用回调函数来隔离变化,所有的读者应该

都会想到这个方法,因为锁无非是具有两个功能——加锁和解锁,我们只要把它抽象成两个回调函数就行了。

  这种方法是可行的。不过这里的情况与前面相比有点特殊。前面的回调函数都是些独立功能的函数,每个回调函数

都有自己独立的上下文,而这里的多个回调函数具有相关的功能,并且共享同一个上下文(锁);其次是这里的上下文

(锁)是一个对象,有自己的生命周期,在完成自己的使命后就应该被销毁。