2015.10.31
读xtcxyczjh(系统程序员成长计划)—- 学习程序设计方法。
之前代码中的内存泄露BUG见3.1。此次抄于读者的读写锁代码保存地址:y15m10d31(rw_locker.c,rw_locker.h)
实现读写锁,要求如下:
- 不依赖于特定平台。
- 在任何情况下都不带来额外的性能开销。
读《xtcxyczjh》P.48 - P.51。
我们能不能实现一种锁,它能串行化对数据结构的修改,而同时支持并行的查询呢?这就是所谓的读写锁,也称为共享-互斥锁。
读写锁的基本要求是:写的时候不允许任何其它线程读或者写,读的时候允许其它线程读,但不允许其它线程写。所以在实现时,写的时候一定要加锁,第一个读的线 程要加锁,后面其它线程读时,只是增加锁的引用计数。
根据准备的内容,新建rw_locker.c文件,基于Locker接口来实现描述读写锁的数据结构。
/* rw_locker.c */
/* 实现读写锁数据结构及接口 */
#include "rw_locker.h"
#include "tk.h"
#include "rw_locker.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
typedef enum _RwLockerModeT {
RW_LOCKER_NONE,
RW_LOCKER_WR,
RW_LOCKER_RD
}RwLockerModeT;
struct _RwLockerT {
int readers; //保护读线程加锁次数的锁
Locker *wr_locker; //写/读线程的锁
Locker *rd_locker; //保护读线程加锁次数的锁
RwLockerModeT mode; //写/读锁被随拥有,写线程还是读线程还是空闲
};
新建rw_locker.h文件,在其中规定/声明读写锁的接口。
/* rw_locker.h */
/* 声明线程锁接口Locker, 声明rw_locker.c中读写锁的接口 */
#ifndef RW_LOCKER_H
#define RW_LOCKER_H
#include "locker.h"
struct _RwLockerT;
typedef struct _RwLockerT RwLockerT;
//读写锁接口
RwLockerT *create_rw_locker(Locker *wr_locker, Locker *rd_locker);
RetT lock_wr_locker(RwLockerT *rw_locker);
RetT lock_rd_locker(RwLockerT *rw_locker);
RetT unlock_rw_locker(RwLockerT *rw_locker);
void free_rw_locker(RwLockerT *rw_locker);
#endif
之前的版本中没有释放经动态分配的线程锁,有内存泄露的BUG(不用急于修复,因为此BUG存在用户代码部分,用户代码最终都会被删掉);而且也没有给用户提供销毁锁的回调函数。
修改Locker接口,增添销毁锁的接口:
/* locker.h */
/* 定义不依赖于平台的线程锁接口 */
//......
typedef void (*LockerDestroyFuncT)(Locker *locker);
//锁接口
struct _Locker {
LockerLockFuncT lock;
LockerUnlockFuncT unlock;
LockerDestroyFuncT destroy;
char dcrt[0];
};
//......
[1] 创建读写锁
/* rw_locker.c */
/* 实现读写锁数据结构及接口 */
#include "rw_locker.h"
#include <stdio.h>
//……
RwLockerT *create_rw_locker(Locker *wr_locker, Locker *rd_locker)
{
RwLockerT *rw_locker = NULL;
return_val_if_p_invalid(NULL != wr_locker && NULL != rd_locker, NULL);
rw_locker = (RwLockerT *)malloc(sizeof(RwLockerT));
if (NULL != rw_locker) {
rw_locker->readers = 0;
rw_locker->mode = RW_LOCKER_NONE;
rw_locker->wr_locker = wr_locker;
rw_locker->rd_locker = rd_locker;
}
return rw_locker;
}
将读写锁创建在堆中,并为所建的所作一些初始化操作。
[2] 写线程加锁
/* rw_locker.c */
/* 实现读写锁数据结构及接口 */
//……
RetT lock_wr_locker(RwLockerT *rw_locker)
{
RetT ret = RET_OK;
return_val_if_p_invalid(NULL != rw_locker, RET_PARAMETER_INVALID);
if ((ret = rw_locker->wr_locker->lock(rw_locker->wr_locker) == RET_OK)) {
rw_locker->mode = RW_LOCKER_WR;
}
return ret;
}
锁Locker指定了lock回调函数(接口)的类型【locker.h中的LockerLockFuncT类型,一个站在linux系统调度实现的一个用户加锁函数是cpthread.c中的lock_nest_thread函数】,用户需要按照这个接口去实现特定平台下的加锁函数并把它赋值给具体的Locker锁,再将被赋值的Locker锁作为参数传递给创建读写锁的函数create_rw_locker后,写时加锁函数即可使用用户编写的加锁函数。
当有读线程操作或者写线程操作处于加锁状态中时,这里的写时加锁语句等待,直到能加锁为止。
[3] 读线程加锁
/* rw_locker.c */
/* 实现读写锁数据结构及接口 */
//……
RetT lock_rd_locker(RwLockerT *rw_locker)
{
RetT ret = RET_OK;
return_val_if_p_invalid(NULL != rw_locker, RET_PARAMETER_INVALID);
if ((ret = rw_locker->rd_locker->lock(rw_locker->rd_locker)) == RET_OK) {
rw_locker->readers++;
if (rw_locker->readers == 1) {
ret = rw_locker->wr_locker->lock(rw_locker->wr_locker);
rw_locker->mode = RW_LOCKER_RD;
}
rw_locker->rd_locker->unlock(rw_locker->rd_locker);
}
return ret;
}
读时加锁接口调用用户编写特定平台下的加锁函数的路线一样。
先尝试为保护读线程加锁次数的变量加锁,增加读线程的加锁次数。如果此时只有一个读线程来读,则加读写锁的读锁,待没有写线程拥有锁时,第一个读线程锁将会加锁成功,并将读写锁的状态设置为读模式(rw_locker->mode = RW_LOCKER_RD;),然后再为保护读线程加锁次数解锁。
[4] 写/读线程的解锁
/* rw_locker.c */
/* 实现读写锁数据结构及接口 */
//……
RetT unlock_rw_locker(RwLockerT *rw_locker)
{
RetT ret = RET_OK;
return_val_if_p_invalid(NULL != rw_locker, RET_PARAMETER_INVALID);
if (rw_locker->mode == RW_LOCKER_WR) {
rw_locker->mode = RW_LOCKER_NONE;
ret = rw_locker->wr_locker->unlock(rw_locker->wr_locker);
}else {
assert(rw_locker->mode == RW_LOCKER_RD);
if ((ret = rw_locker->rd_locker->lock(rw_locker->rd_locker)) == RET_OK) {
rw_locker->readers--;
if (rw_locker->readers == 0) {
rw_locker->mode = RW_LOCKER_NONE;
ret = rw_locker->wr_locker->unlock(rw_locker->wr_locker);
}
rw_locker->rd_locker->unlock(rw_locker->rd_locker);
}
}
return ret;
}
解锁时根据状态来决定,解写读直接解保护受保护对象的锁。解读锁时,先要加锁保护引用计数的锁,引用计数减一。如果自己是最后一个读,才解保护受保护对象的锁,最后解开保护引用计数的锁。
[5] 释放读写锁(销毁锁)
/* rw_locker.c */
/* 实现读写锁数据结构及接口 */
//……
void free_rw_locker(RwLockerT *rw_locker)
{
if (NULL != rw_locker) {
rw_locker->rd_locker->destroy(rw_locker->rd_locker);
rw_locker->wr_locker->destroy(rw_locker->wr_locker);
rw_locker->rd_locker = rw_locker->wr_locker = NULL;
free(rw_locker);
}
}
调用用户销毁锁的函数销毁用户创建的锁。然后再释放在创建读写锁接口内所创建(动态分配)的读写锁内存。
在之前的代码中不包含特定平台下销毁锁的回调函数(对于信号变量来说也不需要)。其实,rw_locker.c和rw_locker.h两个文件实现了读写锁的一个接口。可以通过自动测试的方式来检测这些接口的功能是否正确。调用接口以及实现接口中所要求的回调函数是调用者(使用这个接口的人)的任务,在学习接口或回调函数这个程序设计方法时,主要是明白接口、回调函数的实现机制以及调用用户在特定平台下所实现的回调函数的技术路线。[为不测试以上接口、不调用接口的偷懒行为找个接口]
主要是编译下接口内容,看是否有跟编译或链接相关的错误。
#Makefile
#make命令默认执行make all或者第一条规则
all: main.o dlist.o tk.o autotest.o cpthread.o cfile.o rw_locker.o
$(CC) $^ $(CFLAGS) $@ $(CFLAG_PTHREAD_LIB)
//......
rw_locker.o: rw_locker.c rw_locker.h
$(CC) $(CFLAG_OBJ) $< $(CFLAGS_POSTFIX)
//......
在a目标all后面添加rw_locker.o条件只是为了利用编译器检查下rw_locker.c中有无链接错误。
读写锁要充分发挥作用,就要基于两个假设:
- 读写的不对称性,读的次数远远大于写的次数。像数据库就是这样,决大部分时间是在查询,而修改的情况相对少得多,所以数据库通常使用读写锁。
- 处于临界区的时间比较长。从上面的实现来看,读写锁实际上比正常加/解锁的次数反而要多,如果处于临界区的时间比较短,比如和修改引用计数差不多,使用读写锁,即使全部是读,它的效率也会低于正常锁。
对应需求简述中的“在任何情况下都不带来额外的性能开销”。
读《xtcxyczjh》-Part-8 pnote over.
[2015.10.31-15:41]