读写锁的性能问题及替代方案

本文探讨了读写锁(rwlock)在多线程编程中的应用问题,指出其在实际使用中可能存在的缺陷,并提出了一种使用shared_ptr+mutex实现的copy-on-write模式作为替代方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这两天看了一些资料,谈到了读写锁的性能问题,并建议不要使用读写锁,而采取其他方案代替。

本位首先介绍为什么不适合用读写锁,随后给出了替代读写锁的方案。具体内容如下:


---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

《Linux多线程服务端编程》2.3节这样写道:

读写锁(rwlock)是个看上去很美的抽象,它明确区分了read和write两种行为。

初学者常干的一件事情是,一见到某个共享数据结构频繁读而很少写,就把mutex替换为rwlock。甚至首选rwlock来保护共享状态,这是不正确的。

1、从正确性方面来说,一种典型的易犯错误是在持有read lock的时候修改了共享数据。这通常发生在程序的维护阶段,为了新增功能,程序猿不小心在原来read lock保护的函数中调用了会修改状态的函数。这种错误的后果跟无保护并发读写共享数据是一样的。

2、从性能方面来说,读写锁不见得比普通mutex更高效。无论如何reader lock加锁的开销不会比mutex lock小,因为他要更新当前reader的数目。如果临界区很小,锁竞争不激烈,那么mutex往往会更快。(XXL:如果临界区设置的很大,说明程序本身是有问题的)

3、reader lock可能允许提升(upgrade)为writer lock,也可能不允许提升(Pthread rwlock不允许提升)。如果允许把读锁提升为写锁,后果跟使用recursive mutex(可重入)一样,会造成程序其他问题。如果不允许提升,后果跟使用non-recursive mutex一样,会造成死锁。我宁愿程序死锁,留个“全尸”好查验。

4、通常reader lock是可重入的,writer lock是不可重入的。但是为了防止writer饥饿,writer lock通常会阻塞后来的reader lock,因此reader lock在重入的时候可能死锁。另外,在追求低延迟读取的场合也不适用读写锁。

XXL:补充一下rwlock死锁的问题,线程1获取了读锁,在临界区执行代码;这时,线程2获取写锁,在该锁上等待线程1完成读操作,同事线程2阻塞了后续的读操作;线程1仍在进行剩余读操作,但是它通过函数调用等间接方式,再次获取那个读锁,此时,线程1阻塞,因为线程2已经上了写锁;同时,线程2也在等待线程1释放读锁,才能进行写操作。因此发生了死锁,原因就在于,读锁是可重入的。

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

文章《Real-World Concurrency》(http://queue.acm.org/detail.cfm?id=1454462)也写道:

Be wary of readers/writer locks. If there is a novice error when trying to break up a lock, it is this: seeing that a data structure is frequently accessed for reads and infrequently accessed for writes, one may be tempted to replace a mutex guarding the structure with a readers/writer lock to allow for concurrent readers. This seems reasonable, but unless the hold time for the lock is long, this solution will scale no better (and indeed, may scale worse) than having a single lock. Why? Because the state associated with the readers/writer lock must itself be updated atomically, and in the absence of a more sophisticated (and less space-efficient) synchronization primitive, a readers/writer lock will use a single word of memory to store the number of readers. Because the number of readers must be updated atomically, acquiring the lock as a reader requires the same bus transaction—a read-to-own—as acquiring a mutex, and contention on that line can hurt every bit as much.

There are still many situations where long hold times (e.g., performing I/O under a lock as reader) more than pay for any memory contention, but one should be sure to gather data to make sure that it is having the desired effect on scalability. Even in those situations where a readers/writer lock is appropriate, an additional note of caution is warranted around blocking semantics. If, for example, the lock implementation blocks new readers when a writer is blocked (a common paradigm to avoid writer starvation),one cannot recursively acquire a lock as reader: if a writer blocks between the initial acquisition as reader and the recursive acquisition as reader, deadlock will result when the recursive acquisition is blocked. All of this is not to say that readers/writer locks shouldn’t be used—just that they shouldn’t be romanticized.

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

事实上,我也看了一些博客,通过实验验证mutex的性能要比rwlock好,例如:http://blog.chinaunix.net/uid-28852942-id-3756043.html

不得不说,这篇文章的作者是个傻逼。这种比较只是把mutex和rwlock的读锁进行比较,只开了两个线程,很难体现出并发的效果,就得出“读写锁提高了并行性,但是就速度而言并不比互斥量快”这种结论,荒谬至极。当然,并不是说结论荒谬,而是实验看起来很可笑,拿去忽悠比他还傻的傻逼应该可以。

我认为,要得出这样的结论,应该模拟出这样的业务场景:很多的读线程(至少得几百个吧,但受地址空间限制,32-bit,单进程的线程数一般为300多个,不过可以调节系统的线程栈参数),较少的写线程。在以上的条件下,在给出读临界区很短和读临界区很长这两种情况,这样才能体现出在临界区较短情况下,mutex的优势。当然,临界区较长时,可能读写锁效果会更好,但是临界区较长,本身就是程序设计的问题,你为什不把临界区设置短一点?!

给一个具体实例:

假设Mutex和RWLock做了如下OO封装,并提供了一些基本操作:

class Mutex  // 意会即可,不必深究如何实现,主要用来说明问题
{
public:
    void Init();
    void Destroy();
    void Lock();
    void Unlock();
};

class RWLock
{
public:
    void Init();
    void Destroy();
    void ReadLock();
    void WriteLock();
    void Unlock();
};
现有一个对象,内部有个vector数据,有很多线程读这个数据,很少线程来写。如果用RWLock来实现,则有:

class RaceData_rwlock
{
public:
    RaceData_rwlock()
    {
        vec.resize(100000);
        rwlock.Init();
    }
    ~RaceData_rwlock()
    {
        rwlock.Destroy();
    }
    void Read() const
    {
        rwlock.ReadLock();
        for (vector<int>::const_iterator it = vec.begin(); it != vec.end(); ++it)
        {
            // read (*it) or other read operation
        }
        rwlock.Unlock();
    }
    void Write(int i)
    {
        rwlock.WriteLock();
        vec.push_back(i);
        rwlock.Unlock();
    }
private:
    mutable RWLock rwlock;
    vector<int> vec;
};

如果用Mutex来实现,则有:

class RaceData_mutex
{
public:
    RaceData_mutex()
    {
        vec.resize(100000);
        mutex.Init();
    }
    ~RaceData_mutex()
    {
        mutex.Destroy();
    }
    void Read() const
    {
        mutex.Lock();
        for (vector<int>::const_iterator it = vec.begin(); it != vec.end(); ++it)
        {
            // read (*it) or other read operation
        }
        mutex.Unlock();
    }
    void Write(int i)
    {
        mutex.Lock();
        vec.push_back(i);
        mutex.Unlock();
    }
private:
    mutable Mutex mutex;
    vector<int> vec;
};

由上面可以看出,临界区里需要遍历vec,说明这个临界区还是很长的,既然很多牛人说rwlock性能不好,那么如何在这种情况下找一个方案代替rwlock呢?


我们利用c++ tr1或boost中的shared_ptr + mutex来实现copy-on-write

在此之前,我们必须对Mutex做一个类似RAII的封装,如下:

class MutexLockGuard
{
public:
    explicit MutexLockGuard(Mutex &m)
        : mutex(m)
    {
        m.Lock();
    }
    ~MutexLockGuard()
    {
        mutex.Unlock();
    }
private:
    Mutex &mutex;
};
这样,我们就不用直接调用mutex的Lock和UnLock操作,借助MutexLockGuard对象,在局部作用域结束时,自动析构,从而自动对mutex进行UnLock。


具体替代方案如下,一些重要内容参见代码的注释:

class RaceData_sharedptr
{
public:
    RaceData_sharedptr()
        : dataPtr(new DataType)
    {
        dataPtr->resize(100000);
        mutex.Init();
    }
    ~RaceData_sharedptr()
    {
        mutex.Destroy();
    }
    void Read() const
    {
        DataPtr dataPtrCopy = GetData(); // 创建local shared_ptr,这样就有两个智能指针绑定在真实对象上,引用计数为2
        // 在读取数据的时候没有加锁
        for (vector<int>::const_iterator it = dataPtrCopy->begin(); it != dataPtrCopy->end(); ++it)
        {
            // read (*it) or other read operation
        }
    }//退出Read()时,自动析构DataPtr,对象的引用计数减1
    void Write(int i) // 不会被读操作阻塞太久,因为读操作的临界区很短
    {
        MutexLockGuard lock(mutex);
        if (!dataPtr.unique())
        { // 这里说明有读线程在读取dataPtr
            DataPtr newDataPtr(new DataType(*dataPtr)); // 其实这一步的开销也不小
            dataPtr.swap(newDataPtr);// 替换为新的副本
        }// 在if语句块结束时,自动析构掉newDataPtr,但newDataPtr所指向的这是对象可能不析构,因为该对象的引用计数可能没减为0,可能还有读线程的shared_ptr绑定在该对象上
        dataPtr->push_back(i);
    }
private:
    typedef vector<int> DataType;
    typedef shared_ptr<DataType> DataPtr;
    mutable Mutex mutex;
    DataPtr dataPtr;

    DataPtr GetData() const
    {
        MutexLockGuard lock(mutex);
        return dataPtr;
    }
};

通过上面的实例可以看出,reader线程可能会读到稍旧的数据,如果对一致性要求不这么高的话。

这个程序说实话我没有进行测试,但《Linux多线程服务端编程》的作者陈硕说“据我们测试,大多数情况下更新都是在原来的数据上进行的,拷贝的比例还不到1%,很高效。更准确的说,这不是copy-on-write,而是copy-on-other-reading”。

最后一句话的意思是,当有其他读线程在读的时候,才在写线程中进行copy。有的方案,在写的时候,不论有没有人在读,都全部创建副本,这样的开销还是比较大的,而本文的实现,则是在有人读的情况下才创建副本,没人读的话则不创建,相对来说,开销还是比较小的。

还有一种做法叫做read-copy-update,和本文的做法很相似,我没有去细看,据说不太好理解,具体的网址如下:

http://www.ibm.com/developerworks/cn/linux/l-rcu/

en.wikipedia.org/wiki/Read-copy-update


本文参考信息:

《Linux多线程服务端编程》 by陈硕


<think>我们被要求回答关于Linux内核模块中读写锁的使用方法和示例。根据用户提供的引用,引用[1]提到了自旋锁,但用户现在需要的是读写锁(rwlock)。读写锁与自旋锁类似,但允许多个读者同时访问,而写者需要独占访问。 在Linux内核中,读写锁(rwlock)是一种同步机制,它允许多个读者同时进入临界区,但写者必须独占访问。读写锁适用于读多写少的场景。 读写锁的使用步骤: 1. 定义读写锁变量 2. 初始化读写锁 3. 在读者代码中,使用读锁定和解锁 4. 在写者代码中,使用写锁定和解锁 相关函数(在<linux/rwlock.h>中定义): - 初始化:`rwlock_init(lock)` - 读锁定:`read_lock(lock)`, `read_unlock(lock)` - 写锁定:`write_lock(lock)`, `write_unlock(lock)` 下面给出一个简单的内核模块示例,该模块创建了一个内核线程,模拟读者和写者的操作。 注意:由于内核模块编程涉及系统内核,编写时需格外小心,避免死锁和系统崩溃。 示例模块代码(rwlock_example.c): ```c #include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/kthread.h> #include <linux/delay.h> #include <linux/rwlock.h> static struct task_struct *reader_thread; static struct task_struct *writer_thread; static rwlock_t my_rwlock; // 共享数据 static int shared_data = 0; static int reader_function(void *data) { while (!kthread_should_stop()) { // 获取读锁 read_lock(&my_rwlock); printk(KERN_INFO "Reader: read shared_data = %d\n", shared_data); read_unlock(&my_rwlock); msleep(500); // 模拟其他处理 } return 0; } static int writer_function(void *data) { while (!kthread_should_stop()) { // 获取写锁 write_lock(&my_rwlock); shared_data++; printk(KERN_INFO "Writer: write shared_data = %d\n", shared_data); write_unlock(&my_rwlock); msleep(1000); // 模拟其他处理 } return 0; } static int __init rwlock_init_module(void) { // 初始化读写锁 rwlock_init(&my_rwlock); // 创建读者线程 reader_thread = kthread_run(reader_function, NULL, "reader_thread"); if (IS_ERR(reader_thread)) { printk(KERN_ERR "Failed to create reader thread\n"); return PTR_ERR(reader_thread); } // 创建写者线程 writer_thread = kthread_run(writer_function, NULL, "writer_thread"); if (IS_ERR(writer_thread)) { printk(KERN_ERR "Failed to create writer thread\n"); kthread_stop(reader_thread); return PTR_ERR(writer_thread); } printk(KERN_INFO "rwlock module loaded\n"); return 0; } static void __exit rwlock_cleanup_module(void) { // 停止线程 if (reader_thread) kthread_stop(reader_thread); if (writer_thread) kthread_stop(writer_thread); printk(KERN_INFO "rwlock module unloaded\n"); } module_init(rwlock_init_module); module_exit(rwlock_cleanup_module); MODULE_LICENSE("GPL"); ``` 说明: 1. 模块初始化函数`rwlock_init_module`中,我们初始化读写锁,并创建两个内核线程:一个读者线程和一个写者线程。 2. 读者线程函数`reader_function`循环读取共享数据,每次读取前获取读锁,读取后释放。 3. 写者线程函数`writer_function`循环增加共享数据,每次写入前获取写锁(此时会独占访问),写入后释放。 4. 模块退出函数`rwlock_cleanup_module`负责停止线程。 注意事项: - 在实际应用中,需要确保锁的持有时间尽可能短,避免长时间阻塞。 - 读写锁不适合于读者和写者都非常频繁的场景,因为写者可能会饿死(即长时间无法获取锁)。 - 内核编程中,不能使用可能导致睡眠的函数(如`msleep`)在持有自旋锁(包括读写锁)期间,但在这个例子中,我们在释放锁之后才调用`msleep`,所以是安全的。注意:读写锁属于自旋锁类型,因此在持有锁期间不能睡眠。 另外,Linux内核还提供了顺序锁(seqlock)和RCU(读-复制-更新)等机制,适用于不同的读写场景。 引用说明:虽然用户提供的引用中没有直接提到读写锁,但引用[1]提到了自旋锁,读写锁是自旋锁的一种变体,因此相关概念和注意事项类似[^1]。 §§相关问题§§ 1. 读写锁和自旋锁的主要区别是什么? 2. 在Linux内核模块中,如何避免读写锁导致的写者饥饿问题? 3. 除了读写锁,Linux内核还有哪些读写同步机制?</think>### Linux 内核模块读写锁使用方法与示例 读写锁(rwlock)是Linux内核中用于实现**读/写并发控制**的同步机制,允许多个读者同时访问共享资源,但写者必须独占访问。相比自旋锁,它显著提升了读密集型场景的性能。以下是详细使用方法: --- ### 一、核心头文件与函数 ```c #include <linux/rwlock.h> // 读写锁头文件 ``` | **操作类型** | **函数原型** | **作用** | |--------------|---------------------------------|------------------------------| | 初始化 | `rwlock_init(rwlock_t *lock)` | 初始化读写锁 | | 读锁定 | `read_lock(rwlock_t *lock)` | 获取读锁(允许多读者并发) | | 读解锁 | `read_unlock(rwlock_t *lock)` | 释放读锁 | | 写锁定 | `write_lock(rwlock_t *lock)` | 获取写锁(独占访问) | | 写解锁 | `write_unlock(rwlock_t *lock)` | 释放写锁 | --- ### 二、完整代码示例 ```c #include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/rwlock.h> #include <linux/delay.h> static rwlock_t data_lock; // 声明读写锁 static int shared_data = 0; // 共享资源 // 读者线程函数 static int reader_thread(void *arg) { while (!kthread_should_stop()) { read_lock(&data_lock); // 获取读锁 printk(KERN_INFO "Reader %d: data=%d\n", current->pid, shared_data); read_unlock(&data_lock); // 释放读锁 msleep(100); // 模拟处理延迟 } return 0; } // 写者线程函数 static int writer_thread(void *arg) { while (!kthread_should_stop()) { write_lock(&data_lock); // 获取写锁(独占) shared_data++; printk(KERN_INFO "Writer: updated data to %d\n", shared_data); write_unlock(&data_lock); // 释放写锁 msleep(500); // 模拟写操作耗时 } return 0; } static int __init rwlock_init_module(void) { rwlock_init(&data_lock); // 初始化读写锁 // 创建读者线程(实际项目中需管理线程生命周期) kthread_run(reader_thread, NULL, "reader1"); kthread_run(reader_thread, NULL, "reader2"); // 创建写者线程 kthread_run(writer_thread, NULL, "writer"); printk(KERN_INFO "Module loaded\n"); return 0; } static void __exit rwlock_exit_module(void) { printk(KERN_INFO "Module unloaded\n"); } module_init(rwlock_init_module); module_exit(rwlock_exit_module); MODULE_LICENSE("GPL"); ``` --- ### 三、关键机制解析 1. **读锁特性** - 允许多个读者同时持有读锁 - 当有读者持有锁时,写者会被阻塞 - 示例中两个读者可同时读取`shared_data` 2. **写锁特性** - 写锁是独占的,持有期间阻塞所有读者和其他写者 - 在`writer_thread`中更新数据时,读者线程被暂停 3. **性能优势场景** ```mermaid graph LR A[高频读操作] --> B[读写锁] C[低频写操作] --> B B --> D[高并发性能] ``` 当读操作频率远高于写操作时,读写锁比自旋锁吞吐量提升可达5-10倍[^1]。 --- ### 四、注意事项 1. **死锁风险** - 避免在持有读锁时尝试获取写锁(会导致自死锁) - 解决方案:使用`read_trylock()`/`write_trylock()`非阻塞版本 2. **锁粒度控制** - 最小化锁保护范围:锁内代码执行时间应短于线程调度时间片 - 错误示例:在锁内调用`msleep(1000)`会导致系统卡顿 3. **替代方案选择** | **场景** | **推荐同步机制** | |------------------------|------------------| | 极短临界区(<10μs) | 自旋锁[^1] | | 读多写少 | 读写锁 | | 写操作极少 | RCU机制 | | 需要睡眠的代码 | 互斥锁(mutex) | > 提示:可通过`lockstat`工具分析锁竞争情况,优化性能瓶颈。 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值