一、锁
在书中,我们实现一个锁的背景是:对已经写好的单线程的双向链表做一个改进,使得它可以支持单线程和多线程两种方式。并且满足以下要求:
要求1. 对于多线程版本,由实现者(链表)加锁/解锁,以防调用者忘记解锁造成死锁。
解决思路:这个好办,只要在实现链表的时候在相应的操作函数中加锁即可。
要求2. 区分单线程和多线程版本时,即不需要链接不同的库,也不需要用宏来进行控制,完全可以在运行时切换。
解决思路:
单线程版本调用DList* dlist=dlist_create(NULL, NULL, NULL);
多线程版本调用DList* dlist=dlist_create(NULL, NULL, locker_pthread_create());
要求3. 保持双向链表的通用性,不依赖特定的平台。
解决思路:锁的实现恰恰是依赖平台的(这是“变化的因素”),因此需要“隔离变化”。要隔离变化,自然要用到“回调函数”。这里的回调函数无非加锁/解锁/销毁锁,对于一组相关的回调函数可以把他们整合到一个“接口”中。在下面的代码中我们将看到,C语言的接口中是回调函数(Java的接口中是抽象方法)
综上,思路应该是: 隔离变化 --> 回调函数 --> 接口(其实就是一组相关的回调函数)
二、嵌套锁(和装饰者模式)
上面实现的锁其实完全在多线程环境(单线程呢?废话,单线程还用什么“锁”)下使用,真正使用的应该是这里我们将实现的嵌套锁。因为使用锁的对象是“线程”,而不是“函数”。
设想后一种情况:比如在dlist_insert()中调用了dlist_length(),进入dlist_insert()时已加了一次锁,再调用dlist_length()时又加了一次锁,这时就死锁了。如果调用dlist_length()前先解锁,用完在加锁,就不能保证dlist_insert()的原子性了,因为在这一解一加的过程中,可能其他线程趁虚而入了。
所以,真实情况下使用锁,我们都应该使用这里实现的嵌套锁,因为他对线程进行了加锁。
使用嵌套锁的目的是:指定“使用锁的对象”是线程,这样同一线程的不同函数可以同时使用“锁住的对象”。
调用方式(体会装饰者模式)
Locker* locker=locker_pthread_create(); Locker* nest_locker=locker_nest_create(locker, (TaskSelfFunc)pthread_self); DList* dlist=dlist_create(NULL, NULL, nest_locker);
三、代码导读
这两节是书上的4.2和4.3两节,代码集中在 "$系统程序员成长计划/4/3/locker_nest"中,下面对其中的每个文件做说明:
Makefile: 编译并运行嵌套锁测试
typedef.h: 定义 枚举Ret、宏DECLS_BEGIN、宏DECLS_END、宏return_if_fail(p)、宏return_val_if_fail(p)、SAFE_FREE(p)
dlist.h 和 dlist.c: 双向链表
locker.h: 锁接口(普通锁和嵌套锁均要实现这个接口)
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]; };
locker_pthread.h和locker_pthread.c: 实现锁接口Locker的普通锁。使用了libpthread.so共享库(POSIX Thread函数库, is a POSIX standard for threads)
这里简要的复习一下libpthread库中锁的使用方法——
首先,在编译链接的时候一定要这样做,才能引入pthread共享函数库:
gcc -shared -lpthread dlist.c locker_pthread.c locker_nest.c -o libdlist.so (做成共享库)
或gcc -DDLIST_TEST -lpthread dlist.c locker_pthread.c locker_nest.c -o dlist_test(测试)
#include <pthread.h>
pthread_mutex_t mutex;
1. pthread_mutex_init(&mutex, NULL);
2. pthread_mutex_lock(&mutex);
3. pthread_mutex_unlock(&mutex);
4. pthread_mutex_destroy(&mutex);
locker_nest.h和locker_nest.c: 实现锁接口Locker的嵌套锁,指定使用锁的对象是”线程“。即对于上锁的对象,不允许两个不同的线程同时访问。
int pthread_self(void)可以返回当前线程的id
注意:
1. 区分锁机制中”锁住的对象“和”使用锁的对象“两个概念。
首先来看看锁的使用方法:
Ret dlist_insert(DList* thiz, size_t index, void* data){ return_val_if_fail(thiz != NULL, RET_INVALID_PARAMS); dlist_lock(thiz); ... dlist_unlock(thiz); return ret; }
如果使用locker_pthread实现的普通锁,“锁住的对象”是thiz双向链表对象,但并没有指定“使用锁的对象”,也就是说在加解锁之间不能调用本线程的其他函数。
如果使用locker_nest实现的嵌套锁,“锁住的对象”是thiz双向链表对象,指定“使用锁的对象”是线程,也就是说在加解锁之间可以调用本线程的其他函数(这样,insert()内部就可以调用length()了)。
2. “锁”和“锁住的对象”是两个不同的实体。
1). “锁”是实现Locker的对象
i.e. 具体可以由locker_pthread_create()或locker_nest_create(...)创建
2). “锁住的对象”则是像DList的实例这样的东西,他的创建需要传入一个锁的实例,以便在使用前后加解锁
i.e. 具体可以这样创建:DList* dlist=dlist_create(NULL, NULL, nest_locker);