系统程序员成长计划-并发(四)(下)
转载时请注明出处和作者联系方式
文章出处:http://www.limodev.cn/blog
作者联系方式:李先静 <xianjimli at hotmail dot com>
读写锁
读写锁在加锁时,要区分是为了读而加锁还是为了写而加锁,所以和递归锁不同的是,它无法兼容Locker接口了。不过为了做到不依赖于特定平台,我 们可以利用Locker的接口来抽象锁的实现。利用现有的锁来实现读写锁。读写锁的可变的部分已经被Locker隔离了,所以读写锁本身不需要做成接口。 它只是一个普通对象而已:
struct _RwLocker; typedef struct _RwLocker RwLocker; RwLocker* rw_locker_create(Locker* rw_locker, Locker* rd_locker); Ret rw_locker_wrlock(RwLocker* thiz); Ret rw_locker_rdlock(RwLocker* thiz); Ret rw_locker_unlock(RwLocker* thiz); void rw_locker_destroy(RwLocker* thiz);
o 创建读写锁
RwLocker* rw_locker_create(Locker* rw_locker, Locker* rd_locker) { RwLocker* thiz = NULL; return_val_if_fail(rw_locker != NULL && rd_locker != NULL, NULL); thiz = (RwLocker*)malloc(sizeof(RwLocker)); if(thiz != NULL) { thiz->readers = 0; thiz->mode = RW_LOCKER_NONE; thiz->rw_locker = rw_locker; thiz->rd_locker = rd_locker; } return thiz; }
读写锁的基本要求是:写的时候不允许任何其它线程读或者写,读的时候允许其它线程读,但不允许其它线程写。所以在实现时,写的时候一定要加锁,第一 个读的线程要加锁,后面其它线程读时,只是增加锁的引用计数。我们需要两个锁:一个锁用来保存被保护的对象,一个锁用来保护引用计数。
o 加写锁
Ret rw_locker_wrlock(RwLocker* thiz) { Ret ret = RET_OK; return_val_if_fail(thiz != NULL, RET_INVALID_PARAMS); if((ret = locker_lock(thiz->rw_locker)) == RET_OK) { thiz->mode = RW_LOCKER_WR; } return ret; }
加写锁很简单,直接加保护受保护对象的锁,然后修改锁的状态为已加写锁。后面其它的线程想写,就会这个锁上等待,如果想读也要等待(见后面)。
o 加读锁
Ret rw_locker_rdlock(RwLocker* thiz) { Ret ret = RET_OK; return_val_if_fail(thiz != NULL, RET_INVALID_PARAMS); if((ret = locker_lock(thiz->rd_locker)) == RET_OK) { thiz->readers++; if(thiz->readers == 1) { ret = locker_lock(thiz->rw_locker); thiz->mode = RW_LOCKER_RD; } locker_unlock(thiz->rd_locker); } return ret; }
先尝试加保护引用计数的锁,增加引用计数。如果当前线程是第一个读,就要去加保护受保护对象的锁。如果此时已经有线程在写,就等待直到加锁成功,然后把锁的状态设置为已加读锁,最后解开保护引用计数的锁。
o 解锁
Ret rw_locker_unlock(RwLocker* thiz) { Ret ret = RET_OK; return_val_if_fail(thiz != NULL, RET_INVALID_PARAMS); if(thiz->mode == RW_LOCKER_WR) { thiz->mode == RW_LOCKER_NONE; ret = locker_unlock(thiz->rw_locker); } else { assert(thiz->mode == RW_LOCKER_RD); if((ret = locker_lock(thiz->rd_locker)) == RET_OK) { thiz->readers--; if(thiz->readers == 0) { thiz->mode == RW_LOCKER_NONE; ret = locker_unlock(thiz->rw_locker); } locker_unlock(thiz->rd_locker); } } return ret; }
解锁时根据状态来决定,解写读直接解保护受保护对象的锁。解读锁时,先要加锁保护引用计数的锁,引用计数减一。如果自己是最后一个读,才解保护受保护对象的锁,最后解开保护引用计数的锁。
从上面读写锁的实现,我们可以看出,读写锁要充分发挥作用,就要基于两个假设:
o 读写的不对称性,读的次数远远大于写的次数。像数据库就是这样,决大部分时间是在查询,而修改的情况相对少得多,所以数据库通常使用读写锁。
o 处于临界区的时间比较长。从上面的实现来看,读写锁实际上比正常加/解锁的次数反而要多,如果处于临界区的时间比较短,比如和修改引用计数差不多,使用读写锁,即使全部是读,它的效率也会低于正常锁。