c++并发编程之互斥锁(mutex)的使用方法
1. 多个线程访问同一资源时,为了保证数据的一致性,最简单的方式就是使用 mutex(互斥锁)。
引用 cppreference 的介绍:
The mutex class is a synchronization primitive that can be used to protect shared data from being simultaneously accessed by multiple threads.
方法1:直接操作 mutex,即直接调用 mutex 的 lock / unlock
函数
此例顺带使用了 boost::thread_group
来创建一组线程。
#include <iostream> #include <boost/thread/mutex.hpp> #include <boost/thread/thread.hpp> boost::mutex mutex; int count = 0; void Counter() { mutex.lock(); int i = ++count; std::cout << "count == " << i << std::endl; // 前面代码如有异常,unlock 就调不到了。 mutex.unlock(); } int main() { // 创建一组线程。 boost::thread_group threads; for (int i = 0; i < 4; ++i) { threads.create_thread(&Counter); } // 等待所有线程结束。 threads.join_all(); return 0; }
方法2:使用 lock_guard
自动加锁、解锁。原理是 RAII,和智能指针类似
#include <iostream> #include <boost/thread/lock_guard.hpp> #include <boost/thread/mutex.hpp> #include <boost/thread/thread.hpp> boost::mutex mutex; int count = 0; void Counter() { // lock_guard 在构造函数里加锁,在析构函数里解锁。 boost::lock_guard<boost::mutex> lock(mutex); int i = ++count; std::cout << "count == " << i << std::endl; } int main() { boost::thread_group threads; for (int i = 0; i < 4; ++i) { threads.create_thread(&Counter); } threads.join_all(); return 0; }
方法3:使用 unique_lock
自动加锁、解锁unique_lock
与 lock_guard
原理相同,但是提供了更多功能(比如可以结合条件变量使用)。
注意:mutex::scoped_lock
其实就是 unique_lock<mutex>
的 typedef
。
#include <iostream> #include <boost/thread/mutex.hpp> #include <boost/thread/thread.hpp> boost::mutex mutex; int count = 0; void Counter() { boost::unique_lock<boost::mutex> lock(mutex); int i = ++count; std::cout << "count == " << i << std::endl; } int main() { boost::thread_group threads; for (int i = 0; i < 4; ++i) { threads.create_thread(&Counter); } threads.join_all(); return 0; }
方法4:为输出流使用单独的 mutex
这么做是因为 IO 流并不是线程安全的!
如果不对 IO 进行同步,此例的输出很可能变成:
count == count == 2count == 41 count == 3
因为在下面这条输出语句中:
std::cout << "count == " << i << std::endl;
输出 "count == " 和 i 这两个动作不是原子性的(atomic),可能被其他线程打断。
#include <iostream> #include <boost/thread/mutex.hpp> #include <boost/thread/thread.hpp> #include <boost/thread/lock_guard.hpp> boost::mutex mutex; boost::mutex io_mutex; int count = 0; void Counter() { int i; { boost::unique_lock<boost::mutex> lock(mutex); i = ++count; } { boost::unique_lock<boost::mutex> lock(io_mutex); std::cout << "count == " << i << std::endl; } } int main() { boost::thread_group threads; for (int i = 0; i < 4; ++i) { threads.create_thread(&Counter); } threads.join_all(); return 0; }
2. 保护共享数据的替代设施
2.1 保护共享数据的初始化过程
丑陋的代码:
void undefined_behaviour_with_double_checked_locking() { if(!resource_ptr) // 1 { std::lock_guard<std::mutex> lk(resource_mutex); if(!resource_ptr) // 2 { resource_ptr.reset(new some_resource); // 3 } } resource_ptr->do_something(); // 4 }
这个模式为什么声名狼藉呢?因为这里有潜在的条件竞争,因为外部的读取锁①没有与内部的
写入锁进行同步③。因此就会产生条件竞争,这个条件竞争不仅覆盖指针本身,还会影响到其
指向的对象;即使一个线程知道另一个线程完成对指针进行写入,它可能没有看到新创建的
some_resource实例,然后调用do_something()④后,得到不正确的结果。
C++标准库提供了 std::once_flag 和 std::call_once 来处理这种情况。比起锁住互斥量,并显式的检查指
针,每个线程只需要使用 std::call_once ,在 std::call_once 的结束时,就能安全的知道指
针已经被其他的线程初始化了。使用 std::call_once 比显式使用互斥量消耗的资源更少,特
别是当初始化完成后。
std::shared_ptr<some_resource> resource_ptr; std::once_flag resource_flag; // 1 void init_resource() { resource_ptr.reset(new some_resource); } void foo() { std::call_once(resource_flag,init_resource); // 可以完整的进行一次初始化 resource_ptr->do_something(); }
2.2 保护很少更新的数据结构
虽然更新频度很低,但更新也是有可能发生的,并且当这个可缓存被多个线程访问,这个缓
存就需要适当的保护措施,来对其处于更新状态时进行保护,也为了确保线程读到缓存中的
有效数据。
使用一
个 std::mutex 来保护数据结构,这的确有些反应过度,因为在没有发生修改时,它将削减并
发读取数据的可能性;这里需要另一种不同的互斥量。这种新的互斥量常被称为“读者-写者
锁”(reader-writer mutex),因为其允许两中不同的使用方式:一个“作者”线程独占访问和共
享访问,让多个“读者”线程并发访问。
新的C++标准库应该不提供这样的互斥量,Boost库提供了boost::shared_mutex。
3.3 嵌套锁
C++标准库提供了 std::recursive_mutex 类。其功能与 std::mutex 类似,除了你可以从
同一线程的单个实例上获取多个锁。在互斥量锁住其他线程前,你必须释放你拥有的所有
锁,所以当你调用lock()三次时,你也必须调用unlock()三次。正确使
用 std::lock_guard<std::recursive_mutex> 和 std::unique_lock<std::recursice_mutex> 可以帮
你处理这些问题。