现在的位置: 首页 > 综合 > 正文

系统程序员成长计划-并发(二)(下)

2013年12月02日 ⁄ 综合 ⁄ 共 3144字 ⁄ 字号 评论关闭

面对这个需求,一些初学者可能有点蒙了。以前在学校的时候,对于课本后面的练习,我总是信心百倍,原因很简单,我确信这些练习不管它的出现方式有多么不同,但总是与前面学过的知识有关。记得《如何求解问题—现代启发式方法》中说过,正是这种练习的方式妨碍了我们解决问题的能力,在现实中解决问题时通常没有这么幸运。在《系统程序员成长计划》我把练习放前面,目标就是刺激读者去思考,在学习知识的同时学习解决问题的方法。

这里我们应该怎么分析呢?要在双向链表里加锁,第一是要区分单线程和多线程,要链接同一个库,而且不能用宏来控制。第二是不能依赖于特定平台,而锁本身恰恰又是依赖于平台的。怎么办?很明显这两个需求都要求锁的实现可以变化的:单线程版本它什么都不做,多线程版本中,不同的平台有不同的实现。

我们要做的就是隔离变化。变化怎么隔离?前面我们已经练习过几次用回调函数来隔离变化了,所有的读者都会想到这个方法,因为锁无非是具有两个功能:加锁和解锁,我们把它抽象成两个回调函数就行了。

这种方法是可行的。这里的情况与前面相比有点特殊:前面的回调函数都是些独立功能的函数,每个回调函数都有自己的上下文,而这里的多个回调函数具有相关的功能,并且共享同一个上下文(锁)。其次是这里的上下文(锁)是一个对象,有自己的生命周期,完成自己的使命后就应该被销毁。

这里我们引入接口(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);

接口在软件设计中占有非常重要的地位,它是隔离变化和降低复杂度最有力的武器,差不多所有的设计模式都与接口有关。后面我们会反复的练习,这里请读者仔细体会一下。

本节示例代码请到这里下载。

抱歉!评论已关闭.