互斥锁对于多线程并发编程可以很好的保护数据,但有时效果会出乎意料,比如死锁。
死锁的条件很简单,如同打麻将,四个人都缺同一张牌就和牌,但四个人都不会打出这一张牌,于是就是无尽的消耗。
以下示例是其中一种典型情况,交换两个元素。通常不会有任何问题,但多线程中,如果线程1要交换A,B,线程2要交换B,A,此时为了保护数据,需要加锁,但加锁后则极为尴尬。
顺序加锁的结果是线程1持有锁A,等待锁B,线程2持有锁B,等待锁A,互不相让,就好像麻将桌上等同一张牌。
所以逻辑上必须同时加锁:
#include
#include
#include
struct someBigObject
{
someBigObject() = default;
explicit someBigObject(int64_t rhs)
: l(rhs)
{}
void prt() const
{
std::cout << l << std::endl;
}
private:
friend void swap(someBigObject &lhs, someBigObject &rhs);
int64_t l = 0;
};
void swap(someBigObject &lhs, someBigObject &rhs)
{
std::swap(lhs.l, rhs.l);
}
struct X
{
explicit X(const someBigObject &sd)
: someDetail(sd)
{}
private:
friend void swap(X &lhs, X &rhs);
someBigObject someDetail;
std::mutex m;
};
void swap(X &lhs, X &rhs)
{
if (&lhs == &rhs)
{
return;
}
std::lock(lhs.m, rhs.m);
std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);
std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);
// c++17
// std::scoped_lock guard(lhs.m, rhs.m);
swap(lhs.someDetail, rhs.someDetail);
}
auto main() -> int
{
someBigObject l(8);
someBigObject m(9);
X lx(l);
X mx(m);
swap(lx, mx);
return 0;
}
可能有些意外,没有锁也能导致死锁,例如线程间的相互等待。
比如两个thread 对象,在相互调用join(),但此时又是回到缺一种拍的麻将桌,互相等,谁也不出牌。
不过好在一般人不会这么设计程序,但也难说。
#include
#include
#include
void prt(std::thread &threadX)
{
{
std::cout << "begin\n";
}
threadX.join();
}
extern std::thread threadB;
std::thread threadA(prt, std::ref(threadB));
std::thread threadB(prt, std::ref(threadA));
void funcA()
{
std::cout << "funcA" << std::endl;
threadB.join();
}
void funcB()
{
std::cout << "funcB" << std::endl;
threadA.join();
}
auto main() -> int
{
std::thread threadC = std::thread(funcA);
std::thread threadD = std::thread(funcB);
threadC.join();
threadD.join();
std::cout << "OK" << std::endl;
return 0;
}
最简单的防止死锁方法是每个线程只持有一个锁,此时就不会存在锁的竞争,如果一定要持有多个锁,也要一次性的用std::lock()同时加锁,防止死锁。
当不可避免的使用多个锁,并且封装在不同层次,则务必按照层级次第加锁。
比如要删除一个共享双向链表的某个节点,一个线程从双向链表的左侧加锁,一个线程从双向链表的右侧加锁,最终一定会死锁,所以要从逻辑上杜绝这种设计,如果一定要遍历途中加锁,那么就只能从一个方向加。
在设计之初,划分锁的层级,可以封装出层级锁,虽然不能阻止设计出不按层级加锁的程序,但只要这种不按层次加锁的程序运行,就会抛出异常。
封装时,为了保证每个线程拥有自己的层级,需要设置线程专属变量,
static thread_local uint32_t thisThreadLevelVal;
与以往所有变量不同,这时一个由两个关键字修饰的变量,会建立每个线程独立的变量。
作为共享的层级锁类,如果不加thread_local,则每个线程的层级锁对象的层级改动都会牵扯其他线程,也就无法比较分级了。
以下代码示范 std::thread tb(threadB) 不按层级加锁,外层低levelMutex otherMutex(6000),内层高levelMutex highLevelMutex(10000),抛出异常:
#include
#include
#include
struct levelMutex
{
explicit levelMutex(uint32_t value)
: levelVal(value)
, preLevelVal(0)
{}
void lock()
{
checkForLevelErr();
inMutex.lock();
updateLevelVal();
}
void unlock()
{
if (thisThreadLevelVal != levelVal)
{
throw std::logic_error("mutex level err");
}
thisThreadLevelVal = preLevelVal;
thisValPtr = thisThreadLevelVal;
inMutex.unlock();
}
auto try_lock() -> bool
{
checkForLevelErr();
if (!inMutex.try_lock())
{
return false;
}
updateLevelVal();
return true;
}
private:
void checkForLevelErr() const
{
if (thisThreadLevelVal <= levelVal)
{
throw std::logic_error("mutex level err");
}
}
void updateLevelVal()
{
preLevelVal = thisThreadLevelVal;
thisThreadLevelVal = levelVal;
thisValPtr = thisThreadLevelVal;
fprintf(stdout, "%d\n", thisThreadLevelVal);
}
//内部互斥量
std::mutex inMutex;
//等级值
uint32_t const levelVal;
//前一个等级值
uint32_t preLevelVal;
//本线程等级值
static thread_local uint32_t thisThreadLevelVal;
uint32_t thisValPtr = ULONG_MAX;
};
thread_local uint32_t levelMutex::thisThreadLevelVal(ULONG_MAX);
levelMutex highLevelMutex(10000);
levelMutex lowLevelMutex(5000);
levelMutex otherMutex(6000);
auto doLowLevelStuff() -> int
{
return 1;
}
auto lowLevelFunc() -> int
{
std::lock_guard<levelMutex> lk(lowLevelMutex);
return doLowLevelStuff();
}
void highLevelStuff(int someParam)
{
std::cout << someParam << std::endl;
}
void highLevelFunc()
{
std::lock_guard<levelMutex> lk(highLevelMutex);
highLevelStuff(lowLevelFunc());
}
void threadA()
{
highLevelFunc();
}
void doOtherStuff()
{
std::cout << "other" << std::endl;
}
void otherStuff()
{
highLevelFunc();
doOtherStuff();
}
void threadB()
{
std::lock_guard<levelMutex> lk(otherMutex);
otherStuff();
}
auto main() -> int
{
std::thread ta(threadA);
std::thread tb(threadB);
ta.join();
tb.join();
return 0;
}
多线程死锁问题通常较难解决,很多时候是偶发出现,让人摸不着头脑,所以从设计开始,就应加以注意。