0、前置知识:多线程如何访问共享变量
多线程中对共享变量的访问方式有两种:读和写。其中,写操作是独占的,独占的意思就是,当线程对这个共享变量执行写操作时,其他线程既不能读,也不能写这个变量;而读操作是非独占的,多个线程可以同时读这个共享变量。
一般情况下,多线程访问某个共享变量时,每次访问时都加上一个互斥锁,这样肯定没有问题。**这里多说一句,锁是怎么使用的?**当某个线程执行到某一行代码时,我们希望接下来的代码不被其他线程执行,那么在当前行调用 lock() 函数即可,就好比把接下来的代码放进了一个房间里,这个房间上了锁,只有当前线程执行完这段代码并从房间出来时,这把锁才被打开了。
1、读写锁为何而生
假如现在一个线程 a 只是想读一个共享变量 i,因为不确定是否会有线程去写它,所以我们还是要对它进行加锁。但是这时又有一个线程 b 试图去读共享变量 i,发现被锁定了,那么b不得不等到 a 释放了锁后才能获得锁并读取 i 的值。但是这两个读操作实质上是非独占的,所以我们期望在多个线程试图读取共享变量的时候,它们可以立刻获取因为读而加的锁,而不是需要等待前一个线程释放。当然,如果一个线程用写锁锁住了临界区,那么其他线程无论是读还是写都会发生阻塞。

2、简单读写锁
class readWriteLock {
private:
std::mutex readMtx;
std::mutex writeMtx;
int readCnt; // 已加读锁个数
public:
readWriteLock() : readCnt(0) {}
void readLock()
{
readMtx.lock();
if (++readCnt == 1) {
writeMtx.lock(); // 存在线程读操作时,写加锁(只加一次)
}
readMtx.unlock();
}
void readUnlock()
{
readMtx.lock();
if (--readCnt == 0) { // 没有线程读操作时,释放写锁
writeMtx.unlock();
}
readMtx.unlock();
}
void writeLock()
{
writeMtx.lock();
}
void writeUnlock()
{
writeMtx.unlock();
}
};
代码分析:
- 类 readWriteLock 中定义了两个互斥锁 readMtx 和 writeMtx
- 当读写锁中的读锁被某个线程加上时,先加上读互斥锁,这样保证了其他线程不能再读了;接着,再加上写互斥锁,同时计数加上 1,这样保证了其他线程不能再写了。接着,把读互斥锁释放掉,因为要允许其他线程也能读这个共享变量。也就是说,多次读时,只需要加一把写锁,表明其他线程暂时不能执行写操作。
- 当读写锁中的读锁被某个线程释放时,也是加上了读互斥锁,这样保证多个线程同时释放读写锁中的读锁时没有问题。当没有线程读操作时,释放写互斥锁。表明其他线程可以执行写操作了。
3、简单读写锁的使用
#include <iostream>
#include <vector>
#include <mutex>
#include <thread>
volatile int var = 10; // 保持变量 var 对内存可见性,防止编译器过度优化
readWriteLock rwLock; // 定义全局的读写锁变量
void Write() {
rwLock.writeLock();
var += 10;
std::cout << "write var : " << var << std::endl;
rwLock.writeUnlock();
}
void Read() {
rwLock.readLock();
std::cout << "read var : " << var << std::endl;
rwLock.readUnlock();
}
int main() {
std::vector<std::thread> writers;
std::vector<std::thread> readers;
for (int i = 0; i < 10; i++) { // 10 个写线程
writers.push_back(std::thread(Write)); // std::thread t 的写法报错
}
for (int i = 0; i < 100; i++) { // 100 个读线程
readers.push_back(std::thread(Read));
}
for (auto& t : writers) { // 写线程启动
t.join();
}
for (auto& t : readers) { // 读线程启动
t.join();
}
std::cin.get();
}
程序运行结果如下:

上述代码虽然实现了读写并发的问题,但是无法实现多个线程同时读。关于这个问题,我们可以看看 C++ 14 是怎么处理的。
4、C++14 shared_lock(共享锁)
#include <iostream>
#include <mutex> //unique_lock
#include <shared_mutex> //shared_mutex shared_lock
#include <thread>
std::mutex g_io_mtx;
class ThreadSafeCounter {
private:
mutable std::shared_mutex mutex_;
size_t value_ = 0;
public:
ThreadSafeCounter(){};
~ThreadSafeCounter(){};
size_t get() const {
// 读者, 获取共享锁, 使用shared_lock
std::shared_lock<std::shared_mutex> lck(mutex_);
return value_;
}
size_t increment() {
// 写者, 获取独占锁, 使用unique_lock
std::unique_lock<std::shared_mutex> lck(mutex_);
value_++;
return value_;
}
void reset() {
// 写者, 获取独占锁, 使用unique_lock
std::unique_lock<std::shared_mutex> lck(mutex_);
value_ = 0;
}
};
ThreadSafeCounter counter;
void reader(int id) {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::unique_lock<std::mutex> ulck(g_io_mtx);
std::cout << "reader #" << id << " get value " << counter.get() << "\n";
}
void writer(int id) {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::unique_lock<std::mutex> ulck(g_io_mtx);
std::cout << "writer #" << id << " write value " << counter.increment()
<< "\n";
}
int main() {
std::thread rth[10];
std::thread wth[10];
for (int i = 0; i < 4; i++) {
rth[i] = std::thread(reader, i + 1);
}
for (int i = 0; i < 6; i++) {
wth[i] = std::thread(writer, i + 1);
}
for (int i = 0; i < 4; i++) {
rth[i].join();
}
for (int i = 0; i < 6; i++) {
wth[i].join();
}
return 0;
}
程序运行结果:

5、高并发读写锁
从上一节可以看出,使用 shared_mutex 替代 mutex 就可以实现读并发。
#include <shared_mutex>
class readWriteLock {
private:
std::shared_mutex readMtx;
std::mutex writeMtx;
int readCnt; // 已加读锁个数
public:
readWriteLock() : readCnt(0) {}
void readLock()
{
readMtx.lock();
if (++readCnt == 1) {
writeMtx.lock(); // 存在线程读操作时,写加锁(只加一次)
}
readMtx.unlock();
}
void readUnlock()
{
readMtx.lock();
if (--readCnt == 0) { // 没有线程读操作时,释放写锁
writeMtx.unlock();
}
readMtx.unlock();
}
void writeLock()
{
writeMtx.lock();
}
void writeUnlock()
{
writeMtx.unlock();
}
};
本文介绍了多线程编程中如何安全地访问共享变量,包括使用普通互斥锁、读写锁(如C++14的shared_lock)以及高并发场景下的读写锁实现。重点讲解了读写锁的工作原理和C++14的shared_mutex在并发控制中的应用。
3234

被折叠的 条评论
为什么被折叠?



