Mutex 互斥量(互斥锁) 简介
在 C11 标准程序库头文件中定义了一些互斥访问的类与方法等。mutex 又称互斥量。
1.Mutex 系列类(四种)
①std::mutex : 该类表示普通的互斥锁, 不能递归使用。
std::mutex 是 C++11 中最基本的互斥量,用于保护共享数据免受多个线程同时访问的同步原语。,std::mutex对象提供了独占(排他性)所有权的特性,不支持递归地对 std::mutex 对象上锁
std::mutex 的成员函数
(1).构造函数和析构函数:
std::mutex 不允许拷贝构造,也不允许 move 拷贝,最初产生的 mutex 对象是处于 unlocked 状态的。std::~mutex()销毁互斥。若互斥锁被线程占有,或线程在持有互斥锁的所有权时终止,则行为未定义。
(2)加锁函数:lock(),调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:
①. 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock 之前,该线程一直拥有该锁。
② 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。
③ 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
(3) 尝试加锁 try_lock(),尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况,
① 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。
② 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。
③ 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)
(4)解锁函数: unlock(), 解锁,释放对互斥量的所有权。
std::mutex g_mtx; // 程序执行被创建,全局生存期开始。
void thread_fun()
{
g_mtx.lock(); // 拥有所有权
//
g_mtx.unlock(); // 释放拥有权
}
②std::recursive_mutex:该类表示递归(再入)互斥锁。递归互斥锁可以被同一个线程多次加锁,以获得对互斥锁对象的多层所有权。
该类表示递归(再入)互斥锁。递归互斥锁可以被同一个线程多次加锁,以获得对互斥锁对象的多层所有权。例如,同一个线程多个函数访问临界区时都可以各自加锁,执行后各自解锁。std::recursive_mutex 释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),即 lock()次数和 unlock()次数相同。可见,线程申请递归互斥锁时,如果该递归互斥锁已经被当前调用线程锁住,则不会产生死锁。此外,std::recursive_mutex 的功能与 std::mutex 大致相同。
③ std::time_mutex: 该类表示定时(有限时间)互斥锁,不能递归使用。
std::timed_mutex 比 std::mutex 多了两个成员函数:
(1)try_lock_for():函数参数表示一个时间范围,在这一段时间范围之内线程如果没有获得锁则保持阻塞;如果在此期间其他线程释放了锁,则该线程可获得该互斥锁;如果超时(指定时间范围内没有获得锁),则函数调用返回 false。
(2)try_lock_until():函数参数表示一个时刻,在这一时刻之前线程如果没有获得锁则保持阻塞;如果在此时刻前其他线程释放了锁,则该线程可获得该互斥锁;如果超过指定时刻没有获得锁,则函数调用返回 false
std::timed_mutex mtx; // (有限时间锁)
void fireworks(char ch)
{
while (!mtx.try_lock_for(std::chrono::milliseconds(200)))//200毫秒内没获得锁则返回false
{
std::cout << ch;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
std::cout << "*\n";
mtx.unlock();
}
int main()
{
const int n = 3;
std::thread threads[n];
char ch = 'A';
// spawn n threads:
for (int i = 0; i < n; ++i)
{
threads[i] = std::thread(fireworks, ch + i);
}
for (auto& th : threads)
{
th.join();
}
return 0;
}
④ std::recursive_timed_mutex:带定时的递归互斥锁。
2.Lock 类(四种)
一 :lock_guard (锁保护):\
lock_guard 类不可复制(拷贝和赋值)。只有构造和析构函数。
类 lock_guard 是互斥体包装器,为在作用域块期间占有互斥提供便利 RAII 风格机制。创建 lock_guard 对象时,它试图接收给定互斥的所有权。控制离开创建 lock_guard 对象的作用域时,销毁 lock_guard 并释放互斥。不可用在函数参数传递或者返回过程,只能用在简单的临界区代码段的互斥操作中
std::mutex g_mtx;
void threadfunc(char ch)
{
//首先,这在一个局部作用域内, std::lock_guard 在构造时,
//会调用 g_mutex->lock() 方法;
std::lock_guard<std::mutex> lock(g_mtx);
for (int i = 0; i < 5; ++i)
{
std::lock_guard<std::mutex> lock(g_mtx); //加锁
for (int j = 0; j < 5; ++j)
{ // std::lock_guard<std::mutex> lock(g_mtx);
printf("%c", ch);
}
printf("\n");
}
printf("\n");
//局部作用域代码结束后,
//std::lock_guard 的析构函数会被调用,
//函数中会调用 g_mutex->unlock() 方法。
}
int main()
{
char ch = 'a';
std::thread that[5];
for (int i = 0; i < 5; ++i)
{
that[i] = std::thread(threadfunc, ch + i);
}
for (int i = 0; i < 5; ++i)
{
that[i].join();
}
cout << "Main End" << endl;
return 0;
}
二: scoped_lock (范围):
scoped_lock 类不可复制(拷贝和赋值)。只有构造和析构函数。
类 scoped_lock 是提供便利 RAII 风格机制的互斥包装器,它在作用域块的存在期间占有一或多个互斥。创建scoped_lock 对象时,它试图取得给定互斥的所有权。控制离开创建 scoped_lock 对象的作用域时,析构 scoped_lock 并以逆序释放互斥。若给出数个互斥,则使用免死锁算法,如同以 std::lock 。
三:unique_lock (独占,唯一),
有锁定,修改和观察器函数。
类 unique_lock 是通用互斥包装器,允许延迟锁定、锁定的有时限尝试、递归锁定、所有权转移和条件变量一同使用。类 unique_lock 可移动,但不可复制——它满足可移动构造 (MoveConstructible) 和可移动赋值 (MoveAssignable) 但不满足可复制构造 (CopyConstructible) 或可复制赋值 (CopyAssignable) 。他不仅可以使用在简单的临界区代码段的互斥操作中,还能用在函数调用过程中
std::unique_lock<std::timed_mutex> lock;
四:shared_lock (共享),
有锁定,修改和观察器函数。
类 shared_lock 是通用共享互斥所有权包装器,允许延迟锁定、定时锁定和锁所有权的转移。锁定 shared_lock ,会以共享模式锁定关联的共享互斥( std::unique_lock 可用于以排他性模式锁定)。
shared_lock 类可移动,但不可复制——它满足可移动构造 (MoveConstructible) 与可移动赋值 (MoveAssignable) 的要求,但不满足可复制构造 (CopyConstructible) 或可复制赋值 (CopyAssignable) 。共享所有权模式等待于共享互斥,可使用 std::condition_variable_any ( std::condition_variable 要求std::unique_lock 故而只能以唯一所有权模式等待)。
3.互斥对象管理类模板的加锁策略
std::lock_guard、scoped_lock,std::unique_lock 和 std::shared_lock 类模板在构造时是否加锁是可选的,
C++11 提供了 3 种加锁策略。
下表列出了互斥对象管理类模板对各策略的支持情况。
死锁
死锁是指两个或两个以上的进程(或线程)在运行过程中因争夺资源而造成的一种僵局(Deadly-Embrace) ) ,若无外力作用,这些进程(线程)都将无法向前推进。
std::mutex mtxa;
std::mutex mtxb;
int countA = 1000;
int countB = 1000;
void thread_func1(int money) // A ——-- > B
{
std::scoped_lock lk(mtxa, mtxb); //同时获得两把锁来解决死锁问题
//unique_lock<std::mutex> lca(mtxa); //单用该锁会造成死锁
cout << "线程1获得A账户的锁" << endl;
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
if (countA >= money)
{
cout << "线程1将要获得B账户的锁" << endl;
//unique_lock<std::mutex> lcb(mtxb); //单用该锁会造成死锁
countA -= money;
countB += money;
cout << "转账成功A --—-- > B: " << money << endl ;
}
else {
cout << "A账号余额不足" << endl;
}
}
void thread_func2(int money) // A ——-- > B
{
std::scoped_lock lk(mtxa, mtxb); //同时获得两把锁来解决死锁问题
//unique_lock<std::mutex> lca(mtxb); //单用该锁会造成死锁
cout << "线程2获得B账户的锁" << endl;
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
if (countB >= money)
{
cout << "线程2将要获得A账户的锁" << endl;
//unique_lock<std::mutex> lcb(mtxa); //单用该锁会造成死锁
countB -= money;
countA += money;
cout << "转账成功B --—-- > A: " << money << endl;
}
else {
cout << "B账号余额不足" << endl;
}
}
int main()
{
thread t1(thread_func1, 200);
thread t2(thread_func2, 500);
t1.join();
t2.join();
return 0;
}
死锁产生的 4 个必要条件
1、互斥: 某种资源一次只允许一个 线程 访问,即该资源一旦分配给某个 线程 ,其他 线程 就不能再访问,直到该 线程 访问结束。
2、占有且等待: 一个 线程 本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他 线程 释放该资源。
3、不可抢占: 线程已获得的资源,在末使用完之前,不能强行剥夺。
4、循环等待: 存在一个线程链,使得每个线程都占有下一个线程所需的至少一种资源。
当以上四个条件均满足,必然会造成死锁,发生死锁的进程无法进行下去,它们所持有的资源也无法释放。这样会导致 CPU 的吞吐量下降。所以死锁情况是会浪费系统资源和影响计算机的使用性能的。那么,解决死锁问题就是相当有必要的了。
避免死锁的方法
a 、破坏“占有且等待”条件
b 、破坏“不可抢占”条件
c 、破坏“循环等待”条件