一、mutex类
1.使用线程中可能遇到的问题
来看下面的一段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| #include <iostream> #include <thread>
using std::cin; using std::cout;
int sum = 0; void add() { for (int i = 0; i < 1000000; i++) { sum++; } }
int main() { std::ios::sync_with_stdio(false); cin.tie(nullptr), cout.tie(nullptr);
std::thread t1(add); std::thread t2(add); t1.join(); t2.join(); cout << sum << "\n"; return 0; }
|
运行程序,我们期待它输出2000000,但真实输出却相差甚远,在我的电脑上,它只跑出了一百万出头。
这是为什么?
事实上,由于sum存放在全局空间,所以两个线程共同对这块内存进行操作,如果出现了同时(显然不能说是绝对的同时)对sum加一的操作,计算机只会进行一次加一。因此,在经过多次间隔极短的操作后,程序的输出与我们的期待值大相径庭。
2.mutex互斥量
我们应如何解决这个问题?标准库提供了一个叫做mutex的类,并提供了两个成员函数lock()和unlock():
void lock()
简单来说,给互斥量加锁,当其他线程试图再次调用lock()时,就会被锁住停止,直到互斥量解锁。
附:
锁定互斥体。若另一线程已锁定此互斥体,则对 lock 的调用将阻塞执行,直至获得锁。
若为已占有此 mutex 的线程调用 lock,则行为未定义:例如,程序可能死锁。鼓励能检测非法使用的实现抛出以 resource_deadlock_would_occur 为错误条件的 std::system_error,而不是死锁。
同一互斥体上先前的 unlock() 操作同步于(定义于 std::memory_order)此操作。
void unlock()
给互斥量解锁,否则其他线程无法运行这段代码:
1 2 3 4 5
| std::mutex mtx; ... mtx.lock();
mtx.unlock();
|
这样,code代码只会同时被一个线程所访问,不会出现上述冲突的问题。
附:
解锁互斥体。
互斥体必须被当前执行线程锁定,否则行为未定义。
此操作同步于(定义于 std::memory_order)任何后继的取得同一互斥体所有权的锁定操作。
修改后的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| #include <iostream> #include <thread> #include <mutex>
using std::cin; using std::cout; std::mutex mtx;
int sum = 0; void add() { for (int i = 0; i < 1000000; i++) { mtx.lock(); sum++; mtx.unlock(); } }
int main() { std::ios::sync_with_stdio(false); cin.tie(nullptr), cout.tie(nullptr);
std::thread t1(add); std::thread t2(add); t1.join(); t2.join(); cout << sum << "\n"; return 0; }
|
试着多运行几次,能够发现每次程序输出都是2000000,运行正常。
二、死锁
什么是死锁
先看如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| #include <iostream> #include <thread> #include <mutex>
using std::cin; using std::cout;
std::mutex mtx1, mtx2; void func1() { for (int i = 0; i < 1000; i++) { mtx1.lock(); mtx2.lock(); mtx2.unlock(); mtx1.unlock(); } } void func2() { for (int i = 0; i < 1000; i++) { mtx2.lock(); mtx1.lock(); mtx1.unlock(); mtx2.unlock(); } }
int main() { std::ios::sync_with_stdio(false); cin.tie(nullptr), cout.tie(nullptr);
std::thread t3(func1); std::thread t4(func2); t3.join(); t4.join(); cout << "NoDeadlock\n"; return 0; }
|
多运行几次发现(如果不是上来程序就卡住的情况下),开始会正常输出NoDeadlock,但是之后程序会被卡住(这时候你可以尝试关闭编辑器,或者更聪明点的,打开资源管理器关闭进程)无法输出,遇到这种问题就是发生死锁了。
为什么开始几次能够正常输出(可能吧),之后就会被卡住呢?
让我们分析一下:func1是先给mtx1上锁,后给mtx2上锁;func2是先给mtx2上锁,后给mtx1上锁。
1.如果func1比较超前,先给mtx1和mtx2上锁,之后func2运行时发现mtx2上锁了,于是程序在这里停留一段时间,直到func1向下执行给mtx2解锁,func2又给mtx2上锁,func1给mtx1解锁,func2给mtx1上锁(当然也可能是func1快速解了两个锁,结果都一样,无伤大雅),这似乎顺利的走完了一个流程。
可并不是任何时候都能这么凑巧的:
倘若func1给mtx1上锁后,func2抢在func1给mtx2上锁之前,先给mtx2上了锁,这时候你会发现,func1想向下执行,但发现mtx2被上锁了,func2想向下执行,但func1被上锁了,这样一来,程序就卡死在了这里。
如何避免死锁
我们发现,死锁出现的原因是两个线程都各给一个互斥量上了锁,并且它们之后的程序还有上锁的行为,导致进程受到了阻碍,那我们只需要保证给这两个互斥量上锁的始终是同一个线程,就能够避免这个问题——只需要改变一下上锁的顺序即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| #include <iostream> #include <thread> #include <mutex>
using std::cin; using std::cout;
std::mutex mtx1, mtx2; void func1() { for (int i = 0; i < 1000; i++) { mtx1.lock(); mtx2.lock(); mtx2.unlock(); mtx1.unlock(); } } void func2() { for (int i = 0; i < 1000; i++) { mtx1.lock(); mtx2.lock(); mtx2.unlock(); mtx1.unlock(); } }
int main() { std::ios::sync_with_stdio(false); cin.tie(nullptr), cout.tie(nullptr);
std::thread t3(func1); std::thread t4(func2); t3.join(); t4.join(); cout << "NoDeadlock\n"; return 0; }
|
让我们具象化地思考下这个问题,假如func1比fun2提前给mtx1上了锁,那么fun2就被卡在了这次循环的开始,直到func1完成这次循环,给mtx1解锁后,fun2才能进行本轮循环,反之同理,这样它们内部就有了一种次序,先给mtx1上锁的,一定可以无阻碍地完成本轮循环,而另一个线程只能等第一个线程完成后才能开始它的本轮循环,这样就完美避免了互锁,死锁的问题。