「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 |
|
注意: 同一个线程中,std::lock_guard
和 lock()/unlock()
不能混用,如果用了 std::lock_guard
,就禁止手动再调用 lock()
和 unlock()
,否则会导致死锁或者资源没有正确释放等问题。
2.3 代码示例
1 |
|
在上面的代码中,定义了两个线程 t1
和 t2
,线程函数均为 foo()
,传递的参数均为 100000
,意为对共享数据变量 count
自加 100000
次。在循环每次迭代中,定义一个 std::lock_guard
对象,实现自动加锁和作用域结束后的自动解锁,确保对共享数据的互斥访问。
可能的运行结果如下:
在对数据操作的过程中,打印线程的 id 以及当前变量 count
的值。在输出过程中可以发现,两个线程来回切换进行,表明两个线程是并发(并行)执行的;打印的 count
值保持不断递增,表明我们的两个子线程互斥访问和修改共享数据变量。
最终输出的结果为 200000
,我们的程序正确执行了两个线程 t1
和 t2
同时对 count
进行 100000
次自加修改的操作,对共享数据的访问与修改是互斥的。
3 adopt_lock 参数
std::adopt_lock
是一个标记类型,可以用于 std::lock_guard
的构造函数。当传递这个参数时,表示互斥量已经被锁住,此时对互斥量的锁的管理直接转移给 std::lock_guard
,这样可以确保在合适的时候释放锁,不需要再调用 std::mutex::unlock()
去手动释放锁。
1 |
|
可能的运行结果如下:
与 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() 中的问题相同:
- 一个线程用来接受用户命令(用数字表示命令),并把命令写入一个队列中
- 另一个线程从队列中读取命令,解析命令并执行一系列操作
程序中共享数据(数据结构) msg_que,消息队列同一时刻只允许一个线程进行操作(读取或写入)。因此 msg_que 是临界资源,我们需要对访问临界资源的代码加锁。
我们可以用 std::lock_guard
代替原先代码中的 lock()
和 unlock()
部分。
1 |
|
可能的运行结果如下: