场景
- 在
C/C++
多线程程序里,经常会用到对共享变量进行读写,最容易想到的是加上锁进行读写。假设当有多个线程对共享变量进行读操作时,该业务逻辑的耗时需要至少2s
来进行处理,那么当一个读线程进行锁定时,其他读线程只能等待2s
才会被唤醒。因为读共享变量是线程安全的,有没有方法在读的时候不需要排他锁占用?
说明
- 从
C/C++14
开始,提供了一个std::shared_lock
共享锁,需要传入一个共享互斥量std::shared_mutex
来进行读写锁操作。这个共享锁提升了多线程读共享变量的性能。
static shared_mutex rwMutex;
static string gData;
...
shared_lock<shared_mutex> lock(rwMutex);
std::string str(gData);
std::shared_lock
的作用就是在一个线程获取到共享锁的时候,在没释放前,其他读线程可以继续获取这个共享锁,而写线程想要获取这个mutex
的排他锁定,只能进入等待。
unique_lock<shared_mutex> lock(rwMutex);
gData.append(to_string(gAccumulate++)).append(":");
std::shared_mutex
可以作为std::unique_lock
锁的参数进行排他锁定。 如果有多个写线程,那么其他写线程只能等待第一个写线程操作结束才会被唤醒去获取排他锁。C++
并没有编译时的线程安全检查,默认不禁止数据竞争,如果发生数据竞争时,结果是未定义的,也就是可能导致系统崩溃。 要保证没有数据竞争,只能靠严格的执行库的使用规则,这点即使高级程序员也容易偶尔犯错。希望在未来的标准里可以加上之类编译时检查。还有一点就是尽量在加锁操作时使用
RAII
的特性,比如使用类shared_lock
和unique_lock
来创建局部变量。这样锁定和解锁会成对执行,而不是调用shared_mutex
的lock(),lock_shared()
和unlock(),unlock_shared()
方法。
例子
- 这里写了两个方法来对比了共享锁和排他锁的性能。很明显的,共享锁大大的提高了性能。
// test-shared-lock.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <shared_mutex>
#include <thread>
#include <string>
#include <sstream>
#include <vector>
#include <mutex>
#include <chrono>
#include <condition_variable>
using namespace std;
// 唯一锁
static mutex uMutex;
// 读写锁
static shared_mutex rwMutex;
static string gData;
static mutex rMutex;
static stringstream rData;
static condition_variable rCond;
static int gAccumulate;
static chrono::steady_clock::time_point Cpp11FuncBegin()
{
return chrono::steady_clock::now();
}
static double Cpp11FuncEnd(chrono::steady_clock::time_point t)
{
auto elapse = chrono::steady_clock::now() - t;
chrono::duration<double,milli> mi = elapse;
return mi.count();
}
template<class T,class S>
void FuncReader(int index,S& s)
{
{
std::unique_lock<mutex> lock(rMutex);
rCond.wait(lock);
}
auto tPoint = Cpp11FuncBegin();
//shared_lock<shared_mutex> lock(rwMutex);
T lock(s);
std::string str(gData);
// -- 增加这个是为了看共享锁是否能同时进入; 如果能同时进入那么执行时间大概是2000ms左右.
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
auto result = Cpp11FuncEnd(tPoint);
std::unique_lock<mutex> myLock(rMutex);
rData << "index: " << index << " Read: " << str << " Duration: " << result << "ms \n";
}
static void FuncWriter(int index)
{
{
std::unique_lock<mutex> lock(rMutex);
rCond.wait(lock);
}
unique_lock<shared_mutex> lock(rwMutex);
gData.append(to_string(gAccumulate++)).append(":");
std::unique_lock<mutex> myLock(rMutex);
rData << "index" << index << " Write: " << gData << "\n";
}
// -- 读写共享锁
static void TestReadWriteSharedData()
{
cout << "======== TestReadWriteSharedData ==========" << endl;
rData.clear();
gData.clear();
vector<thread> tasks;
int i = 0;
for (; i < 8; ++i)
tasks.emplace_back(FuncReader<shared_lock<shared_mutex>,shared_mutex>,i,std::ref(rwMutex));
for(; i < 10; ++i)
tasks.emplace_back(FuncWriter,i);
rCond.notify_all();
for (auto& one : tasks)
one.join();
auto str = rData.str();
cout << str << endl;
}
// -- 普通的非共享锁
static void TestUniqueLock()
{
cout << "======== TestUniqueLock ==========" << endl;
rData.clear();
rData.str("");
gData = "0;";
vector<thread> tasks;
int i = 0;
for (; i < 4; ++i)
tasks.emplace_back(FuncReader<unique_lock<mutex>,mutex>,i,std::ref(uMutex));
rCond.notify_all();
for (auto& one : tasks)
one.join();
auto str = rData.str();
cout << str << endl;
}
int main()
{
cout << "Hello World!\n";
TestReadWriteSharedData();
TestUniqueLock();
}
输出
Hello World!
======== TestReadWriteSharedData ==========
index9 Write: 0:
index8 Write: 0:1:
index: 1 Read: 0:1: Duration: 2005.57ms
index: 6 Read: 0:1: Duration: 2005.72ms
index: 0 Read: 0:1: Duration: 2005.57ms
index: 7 Read: 0:1: Duration: 2005.57ms
index: 2 Read: 0:1: Duration: 2005.6ms
index: 3 Read: 0:1: Duration: 2005.63ms
index: 4 Read: 0:1: Duration: 2005.64ms
index: 5 Read: 0:1: Duration: 2005.67ms
======== TestUniqueLock ==========
index: 3 Read: 0; Duration: 2005.35ms
index: 2 Read: 0; Duration: 4012.66ms
index: 0 Read: 0; Duration: 6021.45ms
index: 1 Read: 0; Duration: 8031.52ms