系统程序员成长计划并发(四)(下)

转载时请注明出处和作者联系方式
文章出处: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 处于临界区的时间比较长。从上面的实现来看,读写锁实际上比正常加/解锁的次数反而要多,如果处于临界区的时间比较短,比如和修改引用计数差不多,使用读写锁,即使全部是读,它的效率也会低于正常锁。

本节示例请到这里下载。
原文地址:https://www.cnblogs.com/zhangyunlin/p/6167568.html