一、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();
// code
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上锁的,一定可以无阻碍地完成本轮循环,而另一个线程只能等第一个线程完成后才能开始它的本轮循环,这样就完美避免了互锁,死锁的问题。