cpp11多线程3-mutex

std::mutex

进入多线程编程的世界,除了要牢牢掌握std::thread使用方法,还要掌握互斥量(锁)的使用,这是一种线程同步机制,在C++11中提供了4中互斥量。

1
2
3
4
std::mutex;                  //非递归的互斥量
std::timed_mutex; //带超时的非递归互斥量
std::recursive_mutex; //递归互斥量
std::recursive_timed_mutex; //带超时的递归互斥量

从各种互斥量的名字可以看出其具有的特性,在实际开发中,常用就是std::mutex,它就像是一把锁,我们需要做的就是对它进行加锁与解锁。

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
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>​
std::mutex g_mutex;

void func()
{
std::cout << "entry func test thread ID is : " << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::microseconds(1000));
std::cout << "leave func test thread ID is : " << std::this_thread::get_id() << std::endl;
}

int main()
{
std::thread t1(func);
std::thread t2(func);
std::thread t3(func);
std::thread t4(func);
std::thread t5(func);

t1.join();
t2.join();
t3.join();
t4.join();
t5.join();
return 0;
}

创建了5个线程,然后分别调用func()函数,得到结果:

1
2
3
4
5
6
7
8
9
10
entry func test thread ID is : entry func test thread ID is : 19180
entry func test thread ID is : 3596
13632
entry func test thread ID is : 9520
entry func test thread ID is : 4460
leave func test thread ID is : 13632
leave func test thread ID is : 19180
leave func test thread ID is : leave func test thread ID is : 9520
3596
leave func test thread ID is : 4460

可以看出,并没有按顺序去执行线程函数,后面创建的线程并没有等待前面的线程执行完毕,导致结果混乱,下面用std::mutex进行控制:

保护共享数据的最基本的方式,是使用C++标准库提供的互斥量(头文件)。当访问共享数据前,使用互斥量将相关数据锁住,再当访问结束后,再将数据解锁。线程库需要保证,当一个线程使用特定互斥量锁住共享数据时,其他的线程想要访问锁住的数据,都必须等到之前那个线程对数据进行解锁后,才能进行访问。这就保证了所有线程能看到共享数据,而不破坏不变量。

  C++中通过实例化 std::mutex 创建互斥量,通过调用成员函数lock()进行上锁,unlock()进行解锁。

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
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>​
std::mutex g_mutex;
using namespace std;

void func()
{
g_mutex.lock();

std::cout << "entry func test thread ID is : " << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::microseconds(1000));
std::cout << "leave func test thread ID is : " << std::this_thread::get_id() << std::endl;

g_mutex.unlock();
}
int main()
{
std::thread t1(func);
std::thread t2(func);
std::thread t3(func);
std::thread t4(func);
std::thread t5(func);

t1.join();
t2.join();
t3.join();
t4.join();
t5.join();
return 0;
}

只要线程进入func()函数就进行加锁处理,当线程执行完毕后进行解锁,保证每个线程都能按顺序执行,输出结果:

1
2
3
4
5
6
7
8
9
10
entry func test thread ID is : 8852
leave func test thread ID is : 8852
entry func test thread ID is : 15464
leave func test thread ID is : 15464
entry func test thread ID is : 17600
leave func test thread ID is : 17600
entry func test thread ID is : 16084
leave func test thread ID is : 16084
entry func test thread ID is : 4156
leave func test thread ID is : 4156

虽然通过lock()与unlock()可以解决线程之间的资源竞争问题,但是这里也存在不足。

1
2
3
4
5
6
7
8
9
10
11
12
func()
{
//加锁
执行逻辑处理; //如果该过程抛出异常导致程序退出了,就没法unlock
//解锁

}

int main()
{
......
}

func()中再执行逻辑处理中程序因为某些原因退出了,此时就无法unlock()了,这样其他线程也就无法获取std::mutex,造成死锁现象,其实在加锁之前可以通过trylock()尝试一下能不能加锁。实际开发中,通常也不会这样写代码,而是采用lock_guard来控制std::mutex。

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
template <class _Mutex>
class lock_guard {
public:
using mutex_type = _Mutex;

explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx)
{
_MyMutex.lock(); //构造函数加锁
}

lock_guard(_Mutex& _Mtx, adopt_lock_t) : _MyMutex(_Mtx)
{
}

~lock_guard() noexcept
{
_MyMutex.unlock(); //析构函数解锁
}

lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;

private:
_Mutex& _MyMutex;
};

lock_guard是类模板,在其构造函数中自动给std::mutex加锁,在退出作用域的时候自动解锁,这样就可以保证std::mutex的正确操作,这也是RAII(获取资源便初始化)技术的体现。

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
42
43
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>​
std::mutex g_mutex;

void func()
{
std::lock_guard<std::mutex> lock(g_mutex); //加锁
std::cout << "entry func test thread ID is : " << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::microseconds(1000));
std::cout << "leave func test thread ID is : " << std::this_thread::get_id() << std::endl;
//退出作用域后,lock_guard对象析构就自动解锁
}

int main()
{
std::thread t1(func);
std::thread t2(func);
std::thread t3(func);
std::thread t4(func);
std::thread t5(func);

t1.join();
t2.join();
t3.join();
t4.join();
t5.join();
return 0;
}

运行结果:
entry func test thread ID is : 19164
leave func test thread ID is : 19164
entry func test thread ID is : 15124
leave func test thread ID is : 15124
entry func test thread ID is : 2816
leave func test thread ID is : 2816
entry func test thread ID is : 17584
leave func test thread ID is : 17584
entry func test thread ID is : 15792
leave func test thread ID is : 15792

但互斥量自身也有问题。

  当其中一个成员函数返回的是保护数据的指针或引用时,会破坏对数据的保护。具有访问能力的指针或引用可以访问(并可能修改)被保护的数据,而不会被互斥锁限制。所以切勿将受保护数据的指针或引用传递到互斥锁作用域之外,无论是函数返回值,还是存储在外部可见内存,亦或是以参数的形式传递到用户提供的函数中去。

  另外还会造成死锁,或是对数据保护的太多(或太少)的问题。

参考:

https://www.cnblogs.com/chen-cs/p/13060353.html