「C++ 多线程」std::lock_guard 基本用法

文章大图来源: pixiv_id=114384607

1 问题背景

通常情况下我们手动管理线程的加锁与解锁,会在代码的多个地方正确合理地编写 lock()unlock() 语句。但是,万一我们忘记了加锁、解锁的操作,此时会导致出现互斥量没有及时释放而导致的 死锁 等问题,这会导致程序无法继续执行。而且,频繁地手动进行 lock()unlock() 有时会显得代码 结构比较复杂

2 std::lock_guard

2.1 基本概念及作用

C++ 11 中引入了 std::lock_guard,包含在 <mutex> 中,可以直接取代 lock()unlock() 来管理互斥量 mutex,使得代码的结构更加简洁,提供了一种简单而安全的方式来确保在某个作用域内互斥量被正确地加锁和解锁。

std::lock_guard 是一个类模板,基本的函数原型为 template<class Mutex> class lock_guard,其中 class Mutex 是一个互斥量类型,通常就是 std::mutex

2.2 基本使用方法

在定义 std::lock_guard 对象的位置,std::lock_guard 对象对互斥量进行加锁 lock();在对象析构的位置(作用域结束,通常是 return 的位置),std::lock_guard 对象对互斥量进行解锁 unlock()

假如我们我们有多个线程,并且定义了一个共享数据变量 count,以及一个 std::mutex 互斥量。在线程函数 foo() 中,在 同一作用域 中,对共享变量进行操作之前,可以定义一个 std::lock_guard 对象,此时会自动进行对互斥量加锁;对共享变量操作完,在 作用域结束 的位置,会自动进行解锁。

这样就可以代替 lock()unlock() 的作用,确保多个线程互斥访问共享数据。

例如下面这种形式:

1
2
3
4
5
6
void foo(){
// 定义 lock_guard 对象,自动进行加锁
std::lock_guard<std::mutex> lck(mtx);
count ++ ; // 对共享数据进行操作
// 作用域结束位置(return),自动进行解锁
}

注意: 同一个线程中,std::lock_guardlock()/unlock() 不能混用,如果用了 std::lock_guard,就禁止手动再调用 lock()unlock(),否则会导致死锁或者资源没有正确释放等问题。

2.3 代码示例

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>

std::mutex mtx; // 互斥量
int count;

void foo(int num){
for(int i = 0; i < num; i ++ ){
// lock_guard 代替 lock 和 unlock
std::lock_guard<std::mutex> lck(mtx); // 自动进行 lock()
count ++ ;
std::cout << "thread id = " << std::this_thread::get_id();
std::cout << ", count = " << count << "\n";
// 在定义域结束时,自动进行 unlock()
}
}

int main(){
std::thread t1(foo, 100000);
std::thread t2(foo, 100000);
t1.join();
t2.join();

std::cout << "final count = " << count << "\n";

return 0;
}

在上面的代码中,定义了两个线程 t1t2,线程函数均为 foo(),传递的参数均为 100000,意为对共享数据变量 count 自加 100000 次。在循环每次迭代中,定义一个 std::lock_guard 对象,实现自动加锁和作用域结束后的自动解锁,确保对共享数据的互斥访问。

可能的运行结果如下:

在对数据操作的过程中,打印线程的 id 以及当前变量 count 的值。在输出过程中可以发现,两个线程来回切换进行,表明两个线程是并发(并行)执行的;打印的 count 值保持不断递增,表明我们的两个子线程互斥访问和修改共享数据变量。

最终输出的结果为 200000,我们的程序正确执行了两个线程 t1t2 同时对 count 进行 100000 次自加修改的操作,对共享数据的访问与修改是互斥的。

3 adopt_lock 参数

std::adopt_lock 是一个标记类型,可以用于 std::lock_guard 的构造函数。当传递这个参数时,表示互斥量已经被锁住,此时对互斥量的锁的管理直接转移给 std::lock_guard,这样可以确保在合适的时候释放锁,不需要再调用 std::mutex::unlock() 去手动释放锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>

std::mutex mtx;
int count;

void foo(){
mtx.lock();
std::lock_guard<std::mutex> lck(mtx, std::adopt_lock);
count ++ ;
std::cout << "thread id = " << std::this_thread::get_id();
std::cout << ", count = " << count << "\n";
}

int main(){
std::vector<std::thread> threads(15);
for(auto& t : threads) t = std::thread(foo);
for(auto& t : threads) t.join();

return 0;
}

可能的运行结果如下:

std::unique_lock「C++ 多线程」std::unique_lock 基本用法)不同,std::lock_guard 的构造函数只接受互斥量对象和可选的 std::adopt_lock 参数。它的设计目的是提供一种简单、高效的机制来确保互斥量在作用域结束时被正确释放。而其它比如 std::defer_lock 等参数 std::lock_guard 是不可以使用的。

4 简单应用实例

和之前 std::mutex 文章 「C++ 多线程」std::mutex,lock(), unlock() 中的问题相同:

  1. 一个线程用来接受用户命令(用数字表示命令),并把命令写入一个队列中
  2. 另一个线程从队列中读取命令,解析命令并执行一系列操作

程序中共享数据(数据结构) msg_que,消息队列同一时刻只允许一个线程进行操作(读取或写入)。因此 msg_que 是临界资源,我们需要对访问临界资源的代码加锁。

我们可以用 std::lock_guard 代替原先代码中的 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <queue>

class MyClass{
std::queue<int> msg_que; // 存储用户命令的消息队列
std::mutex mtx; // 互斥量
public:
void in_msg_que(int num){
for(int i = 0; i < num; i ++ ){
std::cout << "in_msg_que() running, push data: " << i << "\n";
// 代替 lock(), unlock()
std::lock_guard<std::mutex> lck(mtx);
msg_que.push(i); // 放入消息队列
}
}

void out_msg_que(int num){
for(int i = 0; i < num; i ++ ){
int command; // 取出的指令
if(pop_command(command)){
// 消息队列不为空,处理取出的指令
std::cout << "out_msg_que() running, command is: " << command << "\n";
}else{
// 消息队列为空
std::cout << "out_msg_que() running, queue is empty: " << i << "\n";
}
}
}

bool pop_command(int &command){
// 代替 lock(), unlock()
std::lock_guard<std::mutex> lck(mtx);
if(msg_que.empty()){
// 如果为空,直接返回false。lck 对象析构,自动进行 unlock()
return false;
}
command = msg_que.front(); // 从队首取出指令
msg_que.pop();
return true; // 返回true,自动 unlock()
}
};

int main(){
MyClass obj;
std::thread out_msg_t(&MyClass::out_msg_que, &obj, 100000);
std::thread in_msg_t(&MyClass::in_msg_que, &obj, 100000);

out_msg_t.join();
in_msg_t.join();

return 0;
}

可能的运行结果如下:


「C++ 多线程」std::lock_guard 基本用法
https://marisamagic.github.io/2024/12/26/20241226/
作者
MarisaMagic
发布于
2024年12月26日
许可协议