「C++ 多线程」std::mutex,lock(), unlock()
文章大图来源: pixiv_id=116700258
1 问题背景
在多线程编程环境中,多个线程可以同时执行不同的任务。当线程需要访问共享资源(例如全局变量、共享的数据结构等),会产生一些问题。例如,有两个线程 t1
和 t2
,都需要访问和修改一个共享的计数器 count
。
如果没有适当的同步机制时,可能会存在 数据竞争。若线程 t1
和 t2
同时修改共享计数器 count
(假设当前计数器 count
的值为 7
),各自对其进行 +1
操作,此时两个线程可能会 同时 将修改的结果(+1
后结果为 8
)写入 count
。最终导致 count
为 8
,然而预期应该是两个线程都对计数器进行 +1
,因此预期最终结果为 9
。这导致了数据的不一致。
2 互斥锁
为了解决多线程并发访问共享资源中数据竞争的问题,我们有了互斥锁(Mutex)。互斥锁基于一种互斥的原则,即 同一时刻 只允许 一个线程访问被保护的资源。
互斥锁的工作原理就类似于一个门锁,只有持有钥匙(获取锁)的线程才能访问房间(共享资源)内的内容。
当一个线程获取互斥锁,其他试图访问共享资源的线程会被 阻塞,直到当前这个获取锁的线程 释放锁。
在之前线程 t1
和 t2
的例子中,如果使用了互斥锁,可以保证 t1
和 t2
对共享数据 count
的修改访问操作是互斥的。当 t1
对 count
进行修改操作,先获得了锁,那么t2
线程会被阻塞,直到 t1
释放锁。反之亦然。这样就确保避免了数据竞争的问题。
3 std::mutex
3.1 基本概念
std::mutex(互斥量 / 互斥锁)是 C++ 标准库中用于实现线程间互斥访问的同步原语。用于多线程中保护共享资源,防止多个线程同时访问和修改导致的数据竞争和数据不一致的问题。
std::mutex 不允许拷贝构造,也不允许 std::move,最初产生的 mutex 对象处于 unlocked 状态。
3.2 lock() 函数
std::mutex::lock()
的功能是 尝试获取互斥量 mutex 的锁。
如果互斥量当前没有被其他线程锁定,那么调用 std::mutex::lock()
的线程将成功获取锁并继续执行后续代码。
如果互斥量已经被其他线程锁定,那么调用 std::mutex::lock()
的线程会被阻塞,直到能够获取锁为止。
3.3 unlock() 函数
std::mutex::unlock()
的功能是 释放互斥量的锁,使得其他等待获取锁的线程有机会获取锁并访问被保护的共享资源。
必须确保 lock 和 unlock 成对出现,并且在正确的逻辑位置调用 unlock。如果一个线程在没有获取锁的情况下调用 unlock,或者多次调用 unlock 而没有相应次数的 lock,会导致未定义行为。
3.4 try_lock() 函数
std::mutex::try_lock()
的功能是 尝试锁住互斥量。
如果获取成功,线程会执行需要互斥访问的代码段,然后通过 unlock() 释放锁。如果获取失败,线程会输出一条消息表示无法获取锁。
4 std::mutex 代码与测试
4.1 示例
1 |
|
在上面的程序中,创建了两个线程分别为 t1
和 t2
,线程函数均为 foo()
,传入的参数均为 100000
,两个线程都将执行对公共数据执行 count
自加 100000
次的任务。
我们同时还定义了一个 std::mutex
互斥量 mtx
,也就是互斥锁。为了让两个线程互斥访问和修改数据,我们需要在每次进行 count
自加修改操作之前进行加锁 mtx.lock()
,在执行完一次 count
自加修改操作后,进行解锁 mtx.unlock()
。
最终我们的 count
变量答案为 200000
。
运行结果如下:
4.2 加锁与解锁位置分析
如果不进行加锁解锁的互斥操作,由于数据竞争可能导致的问题,那么有可能最终答案是一个介于 0 ~ 200000 的中间值:
如果在循环之外加锁解锁,那么本质上就变成第一个线程 t1
先执行完增加 100000
次,然后第二个线程 t2
执行增加 100000
次,而不是并发(并行)执行的。因此本质上就不是多线程了。
1 |
|
在上面的代码中,我们在线程函数的循环之外加锁解锁,那么本质上不是多线程,虽然最终结果也是 200000
,但是脱离了我们要并行执行的本意。
5 简单应用实例
假设我们有如下问题背景:
- 一个线程用来接受用户命令(用数字表示命令),并把命令写入一个队列中
- 另一个线程从队列中读取命令,解析命令并执行一系列操作
程序中共享数据(数据结构) msg_que,也就是消息队列同一时刻只允许一个线程进行操作(读取或写入)。因此 msg_que 是临界资源,我们需要对访问临界资源的代码加锁。
1 |
|
在上面的代码中,定义了一个名为 MyClass 的类,用于管理一个存储整数的 std::queue<int>
消息队列 msg_que
,,并且通过互斥量(std::mutex
)来保证对消息队列操作的线程安全性。在 main 函数中创建了两个线程,分别用于向消息队列中插入数据(in_msg_que
函数)和从消息队列中取出数据(out_msg_que
函数)。
在每次从消息队列中取出数据时,调用 pop_command
函数尝试去除队列中的一个数据。如果队列不为空,那么成功取出并输出消息指令的内容;如果队列为空,那么无法取出数据。
在 pop_command
函数中,先进行加锁,然后判断队列是否为空,如果为空则直接解锁互斥量并返回 false
,表示无法取出数据;如果队列不为空,就将队首的数据赋值给传入的引用参数 command,接着将队首元素从队列中移除,最后再进行解锁,返回 true
,表示成功取出数据。
运行结果如下: