C++读写锁以及实现方式

发布于:2025-06-03 ⋅ 阅读:(19) ⋅ 点赞:(0)

【C++专题】读写锁(Reader-Writer Lock)原理与实现方式(含C++11/20实践)

一、读写锁核心概念
1. 什么是读写锁?

读写锁是一种同步机制,用于控制多个线程对共享资源的访问,其核心特性:

  • 读锁(共享锁):允许多个线程同时获取,适用于只读操作(如读取配置文件、缓存查询)。
  • 写锁(排他锁):仅允许单个线程获取,适用于写操作(如修改数据、更新缓存),此时其他线程(包括读线程)均被阻塞。

适用场景:读多写少的场景(如数据库缓存、日志系统),相比普通互斥锁(Mutex)能显著提升并发性。

2. 读写锁 vs 互斥锁
维度 互斥锁(Mutex) 读写锁(Reader-Writer Lock)
读操作并发 同一时间仅1线程访问 允许多线程同时读
写操作开销 低(简单加锁) 高(需协调读/写线程)
典型场景 读写均衡或写多读少 读多写少(如配置中心、缓存)
二、C++中的读写锁实现方式

方案一:POSIX 读写锁(pthread_rwlock)

适用范围:Linux/macOS等UNIX-like系统,C++11之前的主流方案。

1. 关键接口
#include <pthread.h>

// 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);

// 加读锁(阻塞式)
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
// 尝试加读锁(非阻塞,成功返回0,失败返回EBUSY)
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

// 加写锁(阻塞式)
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
// 尝试加写锁(非阻塞)
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

// 解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

// 销毁锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
2. 使用示例:缓存读取与更新
#include <iostream>
#include <pthread.h>
#include <vector>
#include <thread>

pthread_rwlock_t rwlock;
std::vector<int> cache;

// 读线程:获取读锁,读取缓存
void read_cache(int id) {
    pthread_rwlock_rdlock(&rwlock);
    std::cout << "Thread " << id << " reading: ";
    for (auto val : cache) {
        std::cout << val << " ";
    }
    std::cout << std::endl;
    pthread_rwlock_unlock(&rwlock);
}

// 写线程:获取写锁,更新缓存
void update_cache(int id, std::vector<int> new_data) {
    pthread_rwlock_wrlock(&rwlock);
    cache = new_data;
    std::cout << "Thread " << id << " updated cache." << std::endl;
    pthread_rwlock_unlock(&rwlock);
}

int main() {
    pthread_rwlock_init(&rwlock, nullptr);
    cache = {1, 2, 3};

    // 启动3个读线程
    std::thread readers[3];
    for (int i = 0; i < 3; ++i) {
        readers[i] = std::thread(read_cache, i);
    }

    // 启动1个写线程(会等待读线程释放锁)
    std::thread writer(update_cache, 4, {4, 5, 6});

    // 等待所有线程结束
    for (auto& th : readers) th.join();
    writer.join();
    pthread_rwlock_destroy(&rwlock);
    return 0;
}
3. 注意事项
  • 初始化与销毁:需调用pthread_rwlock_initpthread_rwlock_destroy,避免内存泄漏。
  • 线程安全:写锁具有更高优先级,避免读线程饥饿(某些实现支持“写优先”策略)。

方案二:C++20 共享互斥锁(std::shared_mutex)

适用范围:C++20及以上标准,跨平台(Windows/Linux/macOS)。

1. 核心类
  • std::shared_mutex:底层实现读写锁语义。
  • std::shared_lock:用于获取读锁(共享锁)。
  • std::unique_lock:用于获取写锁(排他锁)。
2. 使用示例:线程安全的计数器
#include <iostream>
#include <shared_mutex>
#include <thread>
#include <vector>

std::shared_mutex rw_mutex;
int counter = 0;

// 读操作:统计计数器值
void read_counter(int id) {
    std::shared_lock<std::shared_mutex> lock(rw_mutex); // 自动获取读锁
    std::cout << "Thread " << id << ": Counter = " << counter << std::endl;
}

// 写操作:增加计数器值
void increment_counter(int id, int times) {
    std::unique_lock<std::shared_mutex> lock(rw_mutex); // 自动获取写锁
    for (int i = 0; i < times; ++i) {
        counter++;
    }
    std::cout << "Thread " << id << " updated counter to " << counter << std::endl;
}

int main() {
    std::vector<std::thread> threads;
    // 启动5个读线程
    for (int i = 0; i < 5; ++i) {
        threads.emplace_back(read_counter, i);
    }
    // 启动2个写线程
    for (int i = 5; i < 7; ++i) {
        threads.emplace_back(increment_counter, i, 100);
    }
    for (auto& th : threads) th.join();
    return 0;
}
3. 优势
  • RAII风格:通过std::shared_lockstd::unique_lock自动管理锁的生命周期,避免忘记解锁。
  • 跨平台兼容:无需依赖POSIX接口,可在Windows(如Visual Studio 2019+)和UNIX系统上编译。

方案三:自定义读写锁(基于互斥量与条件变量)

适用场景:需要理解底层原理,或在不支持C++20的环境中模拟读写锁。

1. 实现原理
  • 读计数器:记录当前获取读锁的线程数,无写锁时允许多线程读。
  • 写锁标志:表示是否有线程持有写锁,写锁获取时阻塞所有读线程。
  • 条件变量:用于写线程等待读线程释放锁,或读线程等待写锁释放。
2. 代码实现(简化版)
#include <mutex>
#include <condition_variable>

class ReadWriteLock {
private:
    std::mutex mtx;
    std::condition_variable read_cv, write_cv;
    int reader_count = 0;
    bool writer_active = false;

public:
    // 获取读锁
    void lock_read() {
        std::unique_lock<std::mutex> lock(mtx);
        // 等待写锁释放
        read_cv.wait(lock, [this]() { return !writer_active; });
        reader_count++;
    }

    // 释放读锁
    void unlock_read() {
        std::unique_lock<std::mutex> lock(mtx);
        reader_count--;
        if (reader_count == 0) {
            write_cv.notify_one(); // 唤醒等待的写线程
        }
    }

    // 获取写锁
    void lock_write() {
        std::unique_lock<std::mutex> lock(mtx);
        // 等待所有读线程释放且无写锁
        write_cv.wait(lock, [this]() { return reader_count == 0 && !writer_active; });
        writer_active = true;
    }

    // 释放写锁
    void unlock_write() {
        std::unique_lock<std::mutex> lock(mtx);
        writer_active = false;
        read_cv.notify_all(); // 唤醒所有读线程
        write_cv.notify_one(); // 唤醒可能等待的写线程
    }
};
3. 关键点
  • 读锁加锁:通过条件变量等待写锁释放,允许多线程同时持有读锁(通过reader_count计数)。
  • 写锁加锁:必须等待所有读锁释放(reader_count==0)且无其他写锁。
  • 优先级策略:此实现为“读优先”,写线程可能饥饿,可通过增加写等待标志优化为“写优先”。
三、读写锁的性能与陷阱
1. 性能对比
操作类型 POSIX读写锁 std::shared_mutex 自定义读写锁(读优先)
读锁加锁耗时
写锁加锁耗时
适用场景 系统级开发 现代C++应用 学习/定制化需求
2. 常见陷阱
  • 死锁:读线程持有读锁时尝试获取写锁,或写线程同时获取多个锁(需避免嵌套加锁)。
  • 饥饿:读线程过多导致写线程长期无法获取锁(可通过“写优先”策略缓解)。
  • 错误解锁:读锁释放时未正确更新计数器,或写锁释放时未通知等待线程。
四、最佳实践建议
  1. 优先使用C++20标准库std::shared_mutexstd::shared_lock提供简洁、安全的读写锁实现,推荐用于新项目。
  2. 明确锁作用域:使用RAII风格的lock_guardunique_lock,避免锁的作用域泄漏。
  3. 性能测试:在高并发场景下,通过基准测试(如Google Benchmark)比较读写锁与无锁编程(如原子操作)的性能差异。
  4. 文档与注释:在代码中注明锁的类型(读锁/写锁)和保护的共享资源,提升可维护性。
五、总结

读写锁是多线程编程中提升读性能的关键工具,其核心在于分离读/写操作的并发策略:

  • POSIX读写锁:适用于UNIX系统的高性能场景,但需手动管理锁生命周期。
  • C++20共享互斥锁:跨平台、安全、简洁,推荐现代C++项目使用。
  • 自定义实现:帮助理解底层原理,但需谨慎处理线程安全和性能问题。

合理使用读写锁能显著优化读多写少场景的并发性,但需注意避免死锁和饥饿问题,结合具体业务场景选择最优方案。

参考资料


网站公告

今日签到

点亮在社区的每一天
去签到