一步一步实现读写锁

多线程编程中,需要对共享变量进行加锁。但是频繁地加锁,会对程序效率有很大影响。在某些读多写少的场景下,多个线程进行读数据时,如果都加互斥锁,这显然是不必须的。于是读写锁便应运而生。

读写锁的加锁规则:

1 如果没有加写锁时,那么多个线程可以同时加读锁;如果有加写锁时,不可以加读锁

2 不管是加了读锁还是写锁,都不能继续加写锁。

满足这两个条件,便可以初步实现一个读写锁。我们用两个锁,一个变量,实现一个简单的读写锁,代码如下

class rwlock
{
public:
	rwlock(): read_cnt(0)
	{
                pthread_mutex_init(&read_mtx,NULL);
                pthread_mutex_init(&write_mtx,NULL);
	}

	~ rwlock()
	{
                pthread_mutex_destroy(&read_mtx);
                pthread_mutex_destroy(&write_mtx);
	}

	void readLock()
	{
		pthread_mutex_lock(&read_mtx);
		if (++read_cnt == 1)
			pthread_mutex_lock(&write_mtx);
		pthread_mutex_unlock(&read_mtx);
	}

	void readUnlock()
	{
		pthread_mutex_lock(&read_mtx);
		if (--read_cnt == 0)
			pthread_mutex_unlock(&write_mtx);
		pthread_mutex_unlock(&read_mtx);
	}

	void writeLock()
	{
		pthread_mutex_lock(&write_mtx);
	}

	void writeUnlock()
	{
		pthread_mutex_unlock(&write_mtx);
	}

private:
	pthread_mutex_t read_mtx;
	pthread_mutex_t write_mtx;
	int read_cnt; // 读锁个数
};

 首先,在加读锁时,判断读者数量,如果为1,说明自己是第一个读者,这时要加写锁。如果没有写者,加锁成功。如果有写者,那么需要等待写锁释放。

其次,加写锁时,就是直接锁write_mtx,如果没有其他任何读者或者写者,加锁成功;否则就等待write_mtx被释放。

这种实现方法简单明了,但是存在一个问题。当读写锁被读者占有时,这时来了写者需要等待读锁释放,如果又来了读锁却可以加锁成功。这样就可能导致,写锁很难获取,读锁一直无法释放。实际应用中,我们并不期望如此,因为这有可能导致数据不能及时更新,读取的数据是过期的。很明显,写锁的优先级应该高于读锁。那么如何实现这样的读写锁呢?

那么在读写锁的数据结构中,应该需要两个变量,来表示在等待的读者和写者的数量。首先给出读写锁的定义

class rwlock
{
public:
    rwlock();
    ~rwlock();
    void readlock();
    void writelock();
    void unlock();
    int tryreadlock();
    int trywritelock();
    
private:
    pthread_mutex_t rwmutex;
    int refcount;   // -1表示有写者,0表示没有加锁,正数表示有多少个读者
    int readwaiters;
    int writewaiters;
    pthread_cond_t readcond;
    pthread_cond_t writecond;
};

 实现如下:

1.构造函数,负责初始化变量

rwlock::rwlock()
{
    refcount     = 0;
    readwaiters  = 0;
    writewaiters = 0;
    pthread_mutex_init(&rwmutex,NULL);
    pthread_cond_init(&readcond, NULL);
    pthread_cond_init(&writecond, NULL);
}

 2 析构函数,销毁资源

rwlock::~rwlock()
{
    refcount     = 0;
    readwaiters  = 0;
    writewaiters = 0;
    pthread_mutex_destroy(&rwmutex);
    pthread_cond_destroy(&readcond);
    pthread_cond_destroy(&writecond);
}

 3 加读锁

void rwlock::readlock()
{
    pthread_mutex_lock(&rwmutex);
    while(refcount < 0)
    {
        readwaiters++;
        pthread_cond_wait(&readcond,&rwmutex);
        readwaiters--;
    }
    refcount++;
    pthread_mutex_unlock(&rwmutex);
}

 首先,对rwmutex加锁,主要是为了读区refcount变量。然后在while循环中,等待读信号量。这里要注意的是,while不能用if来判断。我们可能会认为,在readcond有信号时,说明写者已经释放了写锁,这时refcount必然会等于0,没必要用while循环。但是,请注意,pthread_cond_wait这个函数的执行过程。首先,它会释放锁rwmutex,然后等待readcond有信号,最后获得信号量时,再对rwmutex加锁。这样就会存在一种情况,在readcond获得信号之后,还没来得及对rwmutex进行加锁,另外一个线程这时来获取写锁,很显然它可以获取到,refcount变成了-1。如果不对refcount进行判断就会出错。

4 加写锁

void rwlock::writelock()
{
    pthread_mutex_lock(&rwmutex);
    while(refcount != 0)
    {
        writewaiters++;
        pthread_cond_wait(&writecond,&rwmutex);
        writewaiters--;
    }
    refcount = -1;
    pthread_mutex_unlock(&rwmutex);
}

 注意点和读锁一样,都是要while循环,不再重复

5释放锁

void rwlock::unlock()
{
    pthread_mutex_lock(&rwmutex);
    if(refcount == -1)
        refcount = 0;
    else
        refcount--;
    if(refcount == 0)
    {
        if(writewaiters > 0)
            pthread_cond_signal(&writecond);
        else if(readwaiters > 0)
            pthread_cond_broadcast(&readcond);
    }

    pthread_mutex_unlock(&rwmutex);
}

 解锁时,如果recount==0,说明已经没有任何人再使用读写锁,那么首先判断是否有写锁等待,如果是,置writecond有信号。如果没有写者,只有读者,那么对readcond信号量进行广播。

到这里,读写锁的功能就介绍完了。但是注意上面的加锁接口都是阻塞的,我们接着介绍非阻塞的加锁接口

6 非阻塞读锁

int rwlock::tryreadlock()
{
    int ret = 0;
    pthread_mutex_lock(&rwmutex);
    if(refcount < 0 || writewaiters > 0)
    {
        ret = -1;
    }
    else
        refcount++;
    pthread_mutex_unlock(&rwmutex);
    return ret;
}

7 非阻塞写锁

int rwlock::trywritelock()
{
    int ret = 0;
    pthread_mutex_lock(&rwmutex);
    if(refcount != 0 )
    {
        ret = -1;
    }
    else
        refcount = -1;
    pthread_mutex_unlock(&rwmutex);
    return ret;
}

 非阻塞接口相对比阻塞接口简单,这里就不再重复讲述了。

总结:本文详细介绍了读写锁的功能,以及实现方法。实现都是基于posix接口,适用于所有类unix系统。至于windows系统,可参考这篇博文

http://blog.csdn.net/querw/article/details/7214925

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏iOS开发随笔

iOS Swift 读取本地json文件

2015
来自专栏有趣的django

21天打造分布式爬虫-多线程下载表情包(五)

网址:http://www.doutula.com/photo/list/?page=1

1102
来自专栏Felix的技术分享

《一个操作系统的实现》笔记(5)--内核雏形

2044
来自专栏芋道源码1024

熔断器 Hystrix 源码解析 —— 命令合并执行

本文主要基于 Hystrix 1.5.X 版本 1. 概述 2. HystrixCollapser 2.1 构造方法 2.2 执行命令方式 2.3 核心方法 3...

3817
来自专栏Danny的专栏

机房收费系统(VB.NET)——存储过程实战

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/huyuyang6688/article/...

1745
来自专栏Seebug漏洞平台

Exim Off-by-one(CVE-2018-6789)漏洞复现分析

前段时间meh又挖了一个Exim的RCE漏洞[1],而且这次RCE的漏洞的约束更少了,就算开启了PIE仍然能被利用。虽然去年我研究过Exim,但是时间过去这么久...

5487
来自专栏about云

日志分析实战之清洗日志小实例4:统计网站相关信息

问题导读 1.如何统计网站总的点击量? 2.如何实现统计不能访问网页的个数? 3.文章中如何定义和使用Scala函数的? 导入之后,我们创建Acces...

2853
来自专栏Java3y

客户关系管理系统

前言 为了巩固开发的流程,我们再拿一个客户关系管理系统来练手...! 成果图 我们完成的就是下面的项目! ? ---- 搭建配置环境 配置Tomcat 导入开发...

3245
来自专栏chenssy

【死磕Java并发】—–J.U.C之读写锁:ReentrantReadWriteLock

重入锁ReentrantLock是排他锁,排他锁在同一时刻仅有一个线程可以进行访问,但是在大多数场景下,大部分时间都是提供读服务,而写服务占有的时间较少。然而读...

3307
来自专栏猿天地

nginx转发后后端怎么获取用户真实IP

经常有需求要获取访问用户的IP,在经过nginx转发后真实IP就被隐藏起来了,我们需要在头部信息里拿真实IP,下面是拿IP的代码,考虑了各种情况。 public...

4547

扫码关注云+社区

领取腾讯云代金券