系统程序员成长计划-并发(二)(下)
2,222 views| 2008-12-17| 李先静| 系统程序员成长计划| | 10 条评论转载时请注明出处和作者联系方式
文章出处:http://www.limodev.cn/blog
作者联系方式:李先静 <xianjimli@gmail.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);
接口在软件设计中占有非常重要的地位,它是隔离变化和降低复杂度最有力的武器,差不多所有的设计模式都与接口有关。后面我们会反复的练习,这里请读者仔细体会一下。
本节示例代码请到这里下载。
系统程序员成长计划 Share
Comments
Tags
Recent Posts
Most Viewed
- 系统程序员成长计划写作提纲 - 19,605 views
- Android IPC机制详解 - 6,277 views
- 系统程序员成长计划-走近专业程序员(上) - 6,253 views
- 系统程序员成长计划-写得又快又好的秘诀(一) - 5,390 views
- 系统程序员成长计划-背景知识 - 5,070 views
- i++循环与i–循环的执行效率 - 4,712 views
- 系统程序员成长计划-Write once, run anywhere(WORA)(上) - 4,700 views
- 系统程序员成长计划-走近专业程序员(下) - 4,254 views
- Linux下的调试工具 - 4,017 views
- Advanced Linux Sound Architecture (ALSA) 研究笔记 - 4,017 views
- 系统程序员成长计划-序 - 3,985 views
- 系统程序员成长计划-写得又快又好的秘诀(三) - 3,929 views
- 中国人与自由软件文化研究(搞笑版) - 3,735 views
- Android中的MessageQueue,Handler,Looper和Thread - 3,686 views
- 答复:我不会OOO,仍然可以XXX - 3,658 views
Categories
- Android (28)
- Broncho-A1-Hack (6)
- DirectFB (7)
- FTK(嵌入式GUI) (24)
- GTK+ (29)
- KVM hack notes (8)
- Linux Mobile (65)
- Management (5)
- Mozilla (9)
- Open Source (5)
- Programming (34)
- Tools (9)
- Uncategorized (23)
- Win32 (3)
- X Windows (31)
- 沉思录 (29)
- 系统程序员成长计划 (67)
Blogroll
gallery
Linux guru
推荐网站
Recent Comments
- Dig on 嵌入式GUI FTK设计与实现-事件源(FtkSource)
- 用心生活每一天 » GNU gprof: linux profiling tools 使用 on gcc profiling的工作原理
- JavaScript for: i++ vs i–-传播、沟通、分享-一直“有你” on i++循环与i–循环的执行效率
- Frankly Law on 嵌入式GUI FTK介绍(11)-交叉编译
- tracing on Linux下的调试工具
- ndljsn on FTK移植指南(初稿)
- tracing on 爬塘朗山
- tracing on GTK+(基于DirectFB)的字体处理
- Kely on 系统程序员成长计划写作提纲
- tracing on 爬塘朗山



December 17th, 2008
Joey.Huang
December 17th, 2008
冒出来顶一下,博主是个好的老师。昨天看那个需求时确实觉得有点难,后来也没细想。今天看到答案,恍然大悟。
Joey.Huang
December 17th, 2008
听博主的教诲,从最基础的东西学起。最近在学习GObject,体会到了设计的博大精深,确实是优秀的代码。博主前面讲的自动测试,以及面向对象中的类的继承,接口的定义,接口的继承,多态等所有这些面向对象的概念能用C语言实现得如此优雅。确实是佩服得不行。
Dig
December 18th, 2008
看到昨天题目,自己实现的时候把mutex 创建、加锁、解锁、销毁 的函数指针全部加进了Locker结构。
好像不是很明白为什么要用“接口”,它有什么优势(和四个函数指针的Locker结构相比),能不能请博主指点下。多谢。
admin
December 18th, 2008
呵,谢谢大家的支持。
to dig:我不太明白你的方法,你把代码贴到bbs上我看一下吧。
The linux mobile development » Blog Archive » 系统程序员成长计划写作提纲
May 25th, 2009
[...] 有序数组的应用 第4章 并发与同步 完成 4.1 多线程编程(上)(下) 4.2 同步(上)(下) 4.3 嵌套锁(上)(下) 4.4 读写锁(上)(下) 4.5 无锁(lock-free)数据结构 第5章 [...]
zpcat
September 10th, 2009
真是好东西,看来我是要多看几遍才能领会呀!
air11
December 13th, 2009
将的很好。不过想下载具体代码的时候发现示例代码以及没有下载了?
李先静
December 14th, 2009
到http://www.limodev.cn/projects.html里下载
xiaomeng
January 1st, 2010
你好,我是这样理解多线程的链表使用:
userbi-listlock
这里划分三层:用户层,链表实现层, 线程锁实现层.
对于用户来说,他只想看到链表的相关接口,不关心链表里面的线程锁怎么实现.
如果线程锁也通过回调函数来实现,是不是增加用户的负担?
李先静
January 4th, 2010
可以包装一下。