系统程序员成长计划-并发(二)(下)
转载时请注明出处和作者联系方式
文章出处:http://www.limodev.cn/blog
作者联系方式:李先静 <xianjimli at hotmail dot com>
面对这个需求,一些初学者可能有点蒙了。以前在学校的时候,对于课本后面的练习,我总是信心百倍,原因很简单,我确信这些练习不管 它的出现方式有多么不同,但总是与前面学过的知识有关。记得《如何求解问题—现代启发式方法》中说过,正是这种练习的方式妨碍了我们解决问题的能力,在现 实中解决问题时通常没有这么幸运。在《系统程序员成长计划》我把练习放前面,目标就是刺激读者去思考,在学习知识的同时学习解决问题的方法。
这里我们应该怎么分析呢?要在双向链表里加锁,第一是要区分单线程和多线程,要链接同一个库,而且不能用宏来控制。第二是不能依赖于特定平台,而锁 本身恰恰又是依赖于平台的。怎么办?很明显这两个需求都要求锁的实现可以变化的:单线程版本它什么都不做,多线程版本中,不同的平台有不同的实现。
我们要做的就是隔离变化。变化怎么隔离?前面我们已经练习过几次用回调函数来隔离变化了,所有的读者都会想到这个方法,因为锁无非是具有两个功能:加锁和解锁,我们把它抽象成两个回调函数就行了。
这种方法是可行的。这里的情况与前面相比有点特殊:前面的回调函数都是些独立功能的函数,每个回调函数都有自己的上下文,而这里的多个回调函数具有 相关的功能,并且共享同一个上下文(锁)。其次是这里的上下文(锁)是一个对象,有自己的生命周期,完成自己的使命后就应该被销毁。
这里我们引入接口(interface)这个术语,接口其实就是一个抽象的概念,它只定义调用者和实现者之间的契约,而不规定实现的方法。比如这里 的锁就是一个抽象的概念,它有加锁/解锁两个功能,这是调用者和实现者之间的契约。但光有这个概念不能做任何事情,只有具体的锁才能被使用。至于具体的 锁,不同的平台有不同的实现,但调用者不用关心。正因为调用者不用关心接口的实现方法,接口成了隔离变化最有力的武器。
在这里,锁是一个接口,双向链表是锁的调用者,有基于不同方式实现的锁。通过接口,双向链表把锁的变化隔离开来:区分单线程和多线程,隔离平台相关性。在C语言中,接口的朴素定义是:一组相关的回调函数及其共享的上下文。我们看看锁这个接口怎么定义:
struct _Locker; typedef struct _Locker Locker; typedef Ret (*LockerLockFunc)(Locker* thiz); typedef Ret (*LockerUnlockFunc)(Locker* thiz); typedef void (*LockerDestroyFunc)(Locker* thiz); struct _Locker { LockerLockFunc lock; LockerUnlockFunc unlock; LockerDestroyFunc destroy; char priv[0]; };
这里要注意三个问题:
o 接口一定要足够抽象,不能依赖任何具体实现的数据类型。接口一旦与某个具体实现关联了,另外一种实现就会遇到麻烦。比如这里你使用了pthread_mutex_t,那你要实现一个win32下的锁怎么办呢。
o 接口不能有create函数,但一定要有destroy函数。我们说过对象有自己的生命周期,创建它,使用它,然后销毁它。但接口只是一个概念,不可能通 过这个概念凭空创建一个对象出来,对象只能通过具体实现来创建,所以接口不应该出现create自己的函数。一旦对象被创建出来,使用者应该在不再需要它 时销毁它,在销毁对象时,如果还要知道它的实现方式才能销毁它,那就造成了调用者和实现者之间不必要的耦合,因此接口都要提供一个destroy函数,调 用者可以直接销毁它。
o 这里的priv用来存放上下文信息,也就是具体实现需要用到的数据结构。像前面的回调函数一样,我们可以用一个void* ctx的成员来保存上下文信息。我们使用的char priv[0];技巧,有点额外的好处:只需要一次内存分配,而且可以分配刚好够用的长度(0到任意长度)。
前面我们使用回调函数,调用时要判断回调函数是否为空,每个地方都要重复这个动作,所以我们把这些判断集中起来好了:
static inline Ret locker_lock(Locker* thiz) { return_val_if_fail(thiz != NULL && thiz->lock != NULL, RET_INVALID_PARAMS); return thiz->lock(thiz); } static inline Ret locker_unlock(Locker* thiz) { return_val_if_fail(thiz != NULL && thiz->unlock != NULL, RET_INVALID_PARAMS); return thiz->unlock(thiz); } static inline void locker_destroy(Locker* thiz) { return_if_fail(thiz != NULL && thiz->destroy != NULL); thiz->destroy(thiz); return; }
下面我们来看看基于pthread_mutex的实现:
o 在locker_pthread.h中,提供一个创建函数。
Locker* locker_pthread_create(void);
o 在locker_pthread.c中,实现这些回调函数:
定义私有数据结构:
typedef struct _PrivInfo { pthread_mutex_t mutex; }PrivInfo;
创建对象:
Locker* locker_pthread_create(void) { Locker* thiz = (Locker*)malloc(sizeof(Locker) + sizeof(PrivInfo)); if(thiz != NULL) { PrivInfo* priv = (PrivInfo*)thiz->priv; thiz->lock = locker_pthread_lock; thiz->unlock = locker_pthread_unlock; thiz->destroy = locker_pthread_destroy; pthread_mutex_init(&(priv->mutex), NULL); } return thiz; }
实现几个回调函数:
static Ret locker_pthread_lock(Locker* thiz) { PrivInfo* priv = (PrivInfo*)thiz->priv; int ret = pthread_mutex_lock(&priv->mutex); return ret == 0 ? RET_OK : RET_FAIL; } …
我简单说一下里面几个问题:
o malloc(sizeof(Locker) + sizeof(PrivInfo)); 前面的char priv[0]并不占空间,这是C语言新标准定义的,用于实现变长的buffer,它在这里的长度由sizeof(PrivInfo)决定。
o PrivInfo* priv = (PrivInfo*)thiz->priv; 这里的thiz->priv只是一个定位符,实际上等于(size_t)thiz+sizeof(Locker),帮我们定位到私有数据的内存地址上。
使用方法:
单线程版本:
DList* dlist = dlist_create(NULL, NULL, locker_pthread_create());
多线程版本:
DList* dlist = dlist_create(NULL, NULL, NULL);
接口在软件设计中占有非常重要的地位,它是隔离变化和降低复杂度最有力的武器,差不多所有的设计模式都与接口有关。后面我们会反复的练习,这里请读者仔细体会一下。
本节示例代码请到这里下载。