(第四章 3)锁与嵌套锁的实现

一、锁

      在书中,我们实现一个锁的背景是:对已经写好的单线程的双向链表做一个改进,使得它可以支持单线程和多线程两种方式。并且满足以下要求:

      要求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.hdlist.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.hlocker_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.hlocker_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);

猜你喜欢

转载自chuanwang66.iteye.com/blog/1825030