索引
1 互斥锁
Race Conditions/条件竞争是当两个或多个线程读写某些共享数据,而最后的结果取决于进程运行的精确时序。
想要成功的解决race condition问题,使用互斥锁来保护多线程环境中的共享数据并避免竞争情况。互斥锁又会产生死锁问题。
避免死锁的建议:
- 一是一个线程已经获取一个锁时就不要获取第二个。如果每个线程只有一个锁,锁上就不会产生死锁(但除了互斥锁,其他方面也可能造成死锁,比如即使无锁,线程间相互等待也可能造成死锁)
- 二是持有锁时避免调用用户提供的代码。用户提供的代码可能做任何时,包括获取锁,如果持有锁时调用用户代码获取锁,就会违反第一个建议,并造成死锁。但有时调用用户代码是无法避免的
- 三是按固定顺序获取锁。如果必须获取多个锁且不能用std::lock同时获取,最好在每个线程上用固定顺序获取。上面的例子虽然是按固定顺序获取锁,但如果不同时加锁就会出现死锁,对于这种情况的建议是额外规定固定的调用顺序
- 四是使用层次锁,如果一个锁被低层持有,就不允许再上锁。
2 luck_guard
try_lock、defer_lock、try_lock_for
try_lock()尝试给互斥量加锁,如果拿不到锁,返回false,如果拿到了锁,返回true,这个函数是不阻塞的。
用std::defer_lock的前提是,你不能自己先lock,否则会报异常
std::defer_lock的意思就是并没有给mutex加锁:初始化了一个没有加锁的mutex。
为了仍旧能够使用class std::lock_guard,需要额外加一个参数std::adopt_lock
std::mutex m;
while (m.try_lock() == false) {
doSomeOtherStuff();
}
std::lock_guard<std::mutex> lg(m,std::adopt_lock);
//实例:
std::timed_mutex m;
if (m.try_lock_for(std::chrono::seconds(1))) {
std::lock_guard<std::timed_mutex> lg(m,std::adopt_lock);
...
}
else {
couldNotGetTheLock();
}
注意:如果改变系统时间,时间段和时间点的行为有所不同
luck_guard加adopt参数(提供死锁检查)
std::mutex m1; std::mutex m2; ...
{
std::lock (m1, m2);
std::lock_guard<std::mutex> lockM1(m1,std::adopt_lock);
std::lock_guard<std::mutex> lockM2(m2,std::adopt_lock);
...
}
1.全局函数lock(),会锁住它收到的所有锁(直到全部锁住,或者抛出异常),如果抛出异常,已经上锁的锁,也会全部解锁
2.锁上后,应该使用class std::lock_guard,并带上一个额外的参数std::adopt_lock,表示这个互斥量已经被lock了(你必须要把互斥量提前lock了 ,否者会报异常);
std::adopt_lock标记的效果就是假设调用一方已经拥有了互斥量的所有权(已经lock成功了);通知lock_guard不需要再构造函数中lock这个互斥量了。用std::adopt_lock的前提是,自己需要先把mutex lock上。
注意:lock()提供了死锁的回避机制,也就代表,锁住的顺序不明确
3 unique_lock
调用成员函数owns_lock(),可以检查当前unique_lock东西内的锁,是否锁住(同样,如果锁住unique_lock的析构函数会自动释放锁,否则析构函数什么也不做)
1.构造函数中传递try_to_lock,表示企图锁定,但不希望阻塞
std::unique_lock<std::mutex> lock(mutex,std::try_to_lock); ...
if (lock) {
...
}
2.可以传递一个时间段或时间点给构造函数(尝试在一个时间周期内锁定)
std::unique_lock<std::timed_mutex> lock(mutex,
std::chrono::seconds(1));
...
3.可以传递一个defer_lock,表示初始化这一个锁,但是并未打算锁住
std::unique_lock<std::mutex> lock(mutex,std::defer_lock);
...
lock.lock(); // or (timed) try_lock()
(可以用来建立一个或多个锁,并且在稍后再锁住他们)
(提供release成员函数来释放锁)
1.lock_guard保证锁在离开作用域的时候,会自动释放
mutex可以由lock_guard的构造函数申请,也可以绑定已经锁上的mutex
(lock_guard生命周期内,锁总是保持锁定状态)
lock_guard的操作函数见:表18.9 class lock_guard的操作函数 P1000
2.unique_lock和lock_guard类似,区别是unique_lock的生命周期内,并不是始终保持锁定状态
(你可以明确的控制,unique_lock所控制的锁,是占有锁定状态,还是解锁状态)
如果在调用析构函数的时候,是锁定状态,那析构函数会自动解锁,否则,析构函数什么也不干
lock()可能抛出system_error异常,夹带的差错码和mutex相同
unlock()可能抛出system_error异常,并夹带差错码operation_not_permitted(这个锁并未被锁定)
4 std::lock()同时上锁
避免死锁通常建议让两个mutex以相同顺序上锁,总是先锁A再锁B,但这并不适用所有情况
std::lock解决了此问题,它可以一次性锁住多个mutex,并且没有死锁风险
C++17支持的同时上锁std::scoped_lock
class A {
public:
explicit A(int x) : i(x) {}
int i;
std::mutex m;
};
void f(A& from, A& to, int n)
{
std::lock(from.m, to.m);//同时加锁
//std::scoped_lock l(from.m, to.m);
// 下面按固定顺序加锁,看似不会有死锁的问题
// 但如果没有std::lock同时上锁
// 另一线程中执行f(to, from, n)
// 两个锁的顺序就反了过来,从而可能导致死锁
std::lock_guard<std::mutex> lock1(from.m, std::adopt_lock); // std::adopt_lock表示获取m的所有权
std::lock_guard<std::mutex> lock2(to.m, std::adopt_lock);
from.i -= n;
to.i += n;
}
int main()
{
A x(70);
A y(30);
std::thread t1(f, std::ref(x), std::ref(y), 5);
std::thread t2(f, std::ref(y), std::ref(x), 10);
t1.join();
t2.join();
}
也可以使用std::unique_lock,它比std::lock_guard灵活,可以传入std::adopt_lock管理mutex,也可以传入std::defer_lock表示mutex应保持解锁状态,以使mutex能被std::unique_lock::lock获取,或可以把std::unique_lock传给std::lock
std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);
// std::defer_lock表示不获取m的所有权,因此m还未上锁
std::lock(lock1, lock2); // 此处上锁
std::unique_lock比std::lock_guard占用的空间多,会稍慢一点,如果不需要更灵活的锁,依然可以使用std::lock_guard
5 转移锁的所有权
std::unique_lock<std::mutex> getLock()
{
extern std::mutex m;
std::unique_lock<std::mutex> l(m);
prepareData();
return l; // 不需要std::move(编译器负责调用移动构造函数)
}
void f()
{
std::unique_lock<std::mutex> l(getLock());
doSomething();
}
6 费时操作先解锁
对一些费时的操作(如文件IO)上锁可能造成很多操作被阻塞,可以在面对这些操作时先解锁
void f()
{
std::unique_lock<std::mutex> l(m);
auto data = getData();
l.unlock(); // 费时操作没有必要持有锁,先解锁
auto res = process(data);
l.lock(); // 为了写入数据再次上锁
writeResult(data, res);
}
7 层次锁
class hierarchical_mutex {
std::mutex internal_mutex;
const unsigned long hierarchy_value; // 当前层级值
unsigned long previous_hierarchy_value; // 前一线程的层级值
// 所在线程的层级值,thread_local表示值存在于线程存储期
static thread_local unsigned long this_thread_hierarchy_value;
void check_for_hierarchy_violation() // 检查是否违反层级结构
{
if (this_thread_hierarchy_value <= hierarchy_value)
{
throw std::logic_error("mutex hierarchy violated");
}
}
void update_hierarchy_value()
{
// 先存储当前线程的层级值(用于解锁时恢复)
previous_hierarchy_value = this_thread_hierarchy_value;
// 再把其设为锁的层级值
this_thread_hierarchy_value = hierarchy_value;
}
public:
explicit hierarchical_mutex(unsigned long value) :
hierarchy_value(value), previous_hierarchy_value(0)
{}
void lock()
{
check_for_hierarchy_violation(); // 要求线程层级值大于锁的层级值
internal_mutex.lock(); // 内部锁被锁住
update_hierarchy_value(); // 更新层级值
}
void unlock()
{
if (this_thread_hierarchy_value != hierarchy_value)
{
throw std::logic_error("mutex hierarchy violated");
}
// 恢复前一线程的层级值
this_thread_hierarchy_value = previous_hierarchy_value;
internal_mutex.unlock();
}
bool try_lock()
{
check_for_hierarchy_violation();
if (!internal_mutex.try_lock()) return false;
update_hierarchy_value();
return true;
}
};
thread_local unsigned long // 初始化为ULONG_MAX以使构造锁时能通过检查
hierarchical_mutex::this_thread_hierarchy_value(ULONG_MAX);
简化一下
class A {
std::mutex m; // 内部锁
int val; // 当前层级值
int pre; // 用于保存前一线程层级值
static thread_local int tVal; // tVal存活于一个线程周期
public:
explicit A(int x) : val(x), pre(0) {}
void lock()
{
if (tVal > val)
{ // 存储线程层级值tVal到pre后将其更新为锁的层级值val
m.lock();
pre = tVal;
tVal = val;
}
else
{
throw std::logic_error("mutex hierarchy violated");
}
}
void unlock()
{ // 恢复线程层级值为pre
if (tVal != val)
{
throw std::logic_error("mutex hierarchy violated");
}
tVal = pre;
m.unlock();
}
bool try_lock()
{
if (tVal > val)
{
if (!m.try_lock()) return false;
pre = tVal;
tVal = val;
return true;
}
else
{
throw std::logic_error("mutex hierarchy violated");
}
}
};
thread_local int A::tVal(INT_MAX); // 保证初始构造std::scoped_lock正常
实例
hierarchical_mutex high(10000);
hierarchical_mutex mid(6000);
hierarchical_mutex low(5000); // 构造一个层级锁
// 初始化时锁的层级值hierarchy_value为5000
// previous_hierarchy_value为0
void lf()
{
std::scoped_lock l(low);
// 用层级锁构造std::scoped_lock时会调用low.lock
// lock先检查,this_thread_hierarchy_value初始化为ULONG_MAX
// 因此this_thread_hierarchy_value大于hierarchy_value
// 通过检查,内部锁上锁
// 更新值,把previous_hierarchy_value更新为线程层级值ULONG_MAX
// 把this_thread_hierarchy_value更新为low的层级值5000
} // 调用low.unlock,检查this_thread_hierarchy_value,值为5000
// 与hierarchy_value相等,通过检查
// 接着把this_thread_hierarchy_value恢复为pre保存的ULONG_MAX
// 最后解锁
void hf()
{
std::scoped_lock l(high);
// this_thread_hierarchy_value更新为high的层级值10000
lf(); // 调用lf时,lf里的this_thread_hierarchy_value值为10000
// 过程只是把lf中的注释里this_thread_hierarchy_value初始值改为10000
// 同样能通过检查,其余过程一致,最后解锁lf会恢复到这里的线程层级值10000
}
void mf()
{
std::scoped_lock l(mid);
// this_thread_hierarchy_value更新为mid的层级值6000
hf(); // 调用hf时,hf里的this_thread_hierarchy_value值为6000
// 构造hf里的l时,调用high.lock
// 检查this_thread_hierarchy_value,小于high.hierarchy_value
// 于是throw std::logic_error("mutex hierarchy violated")
}
8 call_once保护共享数据的初始化
一个极端但常见的情况是,共享数据在并发访问和初始化都需要保护,但之后要隐式同步。数据初始化后上锁只是为了保护初始化过程,但这会不必要地影响性能
std::shared_ptr<A> P;
void f()
{
if (!p)
{
// 1. 为A对象分配一片内存
// 2. 在分配的内存上调用A的构造函数,构造一个A对象
// 3. 返回该内存的指针,让p指向该内存
// 编译器不一定按23顺序执行,可能32
p.reset(new A); // 在多线程中这里需要保护,直接上锁会导致不必要的线程资源阻塞
}
p->doSomething();
}
为了处理race condition,C++标准库提供了std::once_flag和std::call_once
每个线程只要使用std::call_once,在std::call_once结束时就能安全地知道指针已被其他线程初始化,而且这比使用mutex的开销更小
#include <iostream>
#include <thread>
#include <mutex>
std::once_flag flag;
void f()
{
std::call_once(flag, [] { std::cout << 1; });
std::cout << 2;
}
int main()
{
std::thread t1(f);
std::thread t2(f);
std::thread t3(f);
t1.join();
t2.join();
t3.join();
}
// output
1222
std::call_once也可以用在类中
class A {
private:
std::once_flag flag;
void init() { ... }
public:
void f()
{
std::call_once(flag, &A::init, this);
...
}
};
C++11规定static变量的初始化只完全发生在一个线程中,直到初始化完成前其他线程都不会做处理,从而避免了race condition。只有一个全局实例时可以不使用std::call_once而直接用static
class A {
public:
static A& getInstance();
A(const A&) = delete;
A& operator(const A&) = delete;
private:
A() = default;
~A() = default;
};
A& A::getInstance()
{
static A instance; // 线程安全的初始化
return instance;
}
9 读写锁
有些数据(比如缓存中存放的DNS入口表)需要经常访问但更新频率很低,如果用std::mutex保护数据有些过度(大量读的操作也会因锁而影响性能),这就需要用上读写锁(reader-writer mutex),它允许多个线程并发读但仅一个线程写
C++17提供了std::shared_mutex和std::shared_timed_mutex(C++14),后者比前者提供了更多操作,但前者性能更高。C++11没有提供读写锁,为此可使用boost::shared_mutex
C++14提供了std::shared_lock,用法和std::unique_lock相同,此外std::shared_lock还允许多线程同时获取共享锁,因此一般用std::shared_lock锁定读,std::unique_lock锁定写
class A {
private:
mutable std::shared_mutex m;
int n = 0;
public:
int read()
{
std::shared_lock<std::shared_mutex> l(m);
return n;
}
void write()
{
std::unique_lock<std::shared_mutex> l(m);
++n;
}
};
10 递归锁std::recursive_mutex()
一个线程已经获取std::mutex(即已上锁)后再次上锁就会产生未定义行为。为了允许这种情况,C++提供了std::recursive_mutex,它可以在一个线程上多次获取锁,但在其他线程获取锁之前必须释放所有的锁
std::mutex m;
void f()
{
m.lock();
m.unlock();
}
void g()
{
m.lock();
f();
m.unlock();
}
int main()
{
std::thread t(g);
t.join(); // 产生未定义行为
}
参考
1.https://downdemo.gitbook.io/cpp-concurrency-in-action-2ed
2.https://wangyi.blog.csdn.net/article/details/104428264
3.https://www.jianshu.com/p/2e82636885cc