文章目录
- 为什么普通互斥锁不够?
- 读写锁的核心规则
- 读者优先和写者优先
- 读者优先
- 写者优先
- Linux读写锁接口
- 初始化和销毁
- 加读锁、加写锁和解锁
- 非阻塞尝试
- 简单用例
- 读写锁和互斥锁怎么选?
当多个线程访问同一份共享数据时,读操作之间通常不会互相修改数据,而写操作必须独占资源。读写锁(Read-Write Lock)针对「读多写少」场景设计:多个读者可以并发读取,但写者进入临界区时必须排他访问。
为什么普通互斥锁不够?
假设共享数据只有一个整数,线程分为两类:
- 读者:只读取数据,不修改数据。
- 写者:修改数据,修改期间不允许其他线程读取或写入。
如果所有操作都使用普通互斥锁,安全性没有问题,但多个读者也必须排队:
读者之间没有数据修改冲突,却被迫串行执行。读写锁能把这种关系表达得更准确:读者共享,写者独占。
读写锁的核心规则
| 模式 | 同时允许的线程 | 典型用途 |
|---|---|---|
| 读锁(共享锁) | 多个读者 | 查询、遍历、读取配置 |
| 写锁(独占锁) | 一个写者,且不能有读者 | 更新、插入、删除 |
必须牢记:
- 写者持有写锁时,不能有其他读者或写者进入临界区。
- 读者持有读锁时,其他读者可以并发进入,但写者必须等待。
- 每次成功加锁都必须对应一次解锁。
读者优先和写者优先
读者优先
只要当前有读者,新到达的读者通常可以继续获得读锁。读吞吐量较高,但如果读者持续到达,写者可能长时间得不到机会,形成「写者饥饿」。
写者优先
一旦有写者等待,后续读者会被暂缓,让写者尽快完成更新。这样可以降低写者等待时间,但写者持续到达时,读者可能长时间等待。
Linux读写锁接口
初始化和销毁
pthread_rwlock_t rwlock;pthread_rwlock_init(&rwlock,nullptr);// 创建并运行线程pthread_rwlock_destroy(&rwlock);销毁前必须确保没有线程仍在使用该锁。
加读锁、加写锁和解锁
pthread_rwlock_rdlock(&rwlock);// 申请读锁// 读取共享数据pthread_rwlock_unlock(&rwlock);pthread_rwlock_wrlock(&rwlock);// 申请写锁// 修改共享数据pthread_rwlock_unlock(&rwlock);读者调用rdlock,多个读者可以同时持有读锁;写者调用wrlock,必须等待所有读者退出,并独占临界区。
非阻塞尝试
intrc1=pthread_rwlock_tryrdlock(&rwlock);intrc2=pthread_rwlock_trywrlock(&rwlock);如果锁不可用,函数会立即返回错误码。使用try*时必须处理失败分支,不能把「未获得锁」当成「已经进入临界区」。
简单用例
#include<pthread.h>intshared_data=0;pthread_rwlock_t rwlock;void*reader(void*){pthread_rwlock_rdlock(&rwlock);intvalue=shared_data;pthread_rwlock_unlock(&rwlock);returnnullptr;}void*writer(void*){pthread_rwlock_wrlock(&rwlock);++shared_data;pthread_rwlock_unlock(&rwlock);returnnullptr;}代码:
#include<pthread.h>#include<unistd.h>#include<atomic>#include<cstdlib>#include<iostream>#include<mutex>#include<random>#include<thread>#include<vector>namespace{constexprintkReaderCount=4;constexprintkWriterCount=2;constexprintkRoundsPerThread=8;intshared_data=0;pthread_rwlock_t rwlock;std::atomic<int>active_readers{0};std::atomic<int>active_writers{0};std::atomic<int>max_concurrent_readers{0};std::atomic<int>max_concurrent_writers{0};std::atomic<int>overlap_count{0};std::atomic<int>write_count{0};std::mutex output_mutex;voidupdate_max(std::atomic<int>&target,intvalue){intprevious=target.load(std::memory_order_relaxed);while(previous<value&&!target.compare_exchange_weak(previous,value,std::memory_order_relaxed,std::memory_order_relaxed)){}}voidreader_task(intid){for(intround=0;round<kRoundsPerThread;++round){if(pthread_rwlock_rdlock(&rwlock)!=0){std::lock_guard<std::mutex>lock(output_mutex);std::cerr<<"读者 "<<id<<" 获取读锁失败\n";return;}constintreaders=active_readers.fetch_add(1)+1;update_max(max_concurrent_readers,readers);if(active_writers.load()!=0){++overlap_count;}constintvalue=shared_data;usleep(5'000);active_readers.fetch_sub(1);pthread_rwlock_unlock(&rwlock);if(round==0){std::lock_guard<std::mutex>lock(output_mutex);std::cout<<"读者-"<<id<<" 读取到 "<<value<<'\n';}std::this_thread::yield();}}voidwriter_task(intid){std::mt19937generator(static_cast<unsigned>(id+17));std::uniform_int_distribution<int>distribution(1,9);for(intround=0;round<kRoundsPerThread;++round){if(pthread_rwlock_wrlock(&rwlock)!=0){std::lock_guard<std::mutex>lock(output_mutex);std::cerr<<"写者 "<<id<<" 获取写锁失败\n";return;}constintwriters=active_writers.fetch_add(1)+1;update_max(max_concurrent_writers,writers);if(writers!=1||active_readers.load()!=0){++overlap_count;}shared_data+=distribution(generator);++write_count;constintvalue=shared_data;usleep(8'000);active_writers.fetch_sub(1);pthread_rwlock_unlock(&rwlock);if(round==0){std::lock_guard<std::mutex>lock(output_mutex);std::cout<<"写者-"<<id<<" 写入后为 "<<value<<'\n';}std::this_thread::yield();}}}// namespaceintmain(){if(pthread_rwlock_init(&rwlock,nullptr)!=0){std::cerr<<"读写锁初始化失败\n";returnEXIT_FAILURE;}std::cout<<"配置:读者 "<<kReaderCount<<" 个,写者 "<<kWriterCount<<" 个,每个线程 "<<kRoundsPerThread<<" 轮\n";std::vector<std::thread>readers;std::vector<std::thread>writers;readers.reserve(kReaderCount);writers.reserve(kWriterCount);for(inti=0;i<kReaderCount;++i){readers.emplace_back(reader_task,i);}for(inti=0;i<kWriterCount;++i){writers.emplace_back(writer_task,i);}for(auto&thread:readers){thread.join();}for(auto&thread:writers){thread.join();}pthread_rwlock_destroy(&rwlock);constintexpected_writes=kWriterCount*kRoundsPerThread;constboolpassed=max_concurrent_writers.load()==1&&overlap_count.load()==0&&write_count.load()==expected_writes;std::cout<<"最大并发读者数:"<<max_concurrent_readers.load()<<'\n';std::cout<<"最大并发写者数:"<<max_concurrent_writers.load()<<'\n';std::cout<<"读写重叠次数:"<<overlap_count.load()<<'\n';std::cout<<"写入次数:"<<write_count.load()<<'\n';std::cout<<"最终共享数据:"<<shared_data<<'\n';std::cout<<"验证结果:"<<(passed?"通过":"失败")<<'\n';returnpassed?EXIT_SUCCESS:EXIT_FAILURE;}运行结果:
配置:读者 4 个,写者 2 个,每个线程 8 轮 读者-0 读取到 0 读者-2 读取到 0 读者-1 读取到 0 读者-3 读取到 0 写者-0 写入后为 3 写者-1 写入后为 43 最大并发读者数:4 最大并发写者数:1 读写重叠次数:0 写入次数:16 最终共享数据:75 验证结果:通过读写锁和互斥锁怎么选?
| 条件 | 更适合的工具 |
|---|---|
| 临界区很短,读写比例接近 | std::mutex或pthread_mutex_t |
| 读操作远多于写操作,读者可以安全并发 | pthread_rwlock_t |
| 需要 C++ 风格的共享互斥量 | std::shared_mutex |
| 共享资源支持无锁算法 | 原子操作或专用并发容器 |
读写锁不一定比互斥锁快。它需要维护读者计数和等待队列;如果临界区很短、竞争不明显,普通互斥锁可能更简单。选择前应使用真实负载进行测量。