#include <thread> #include <set> #include <random> using namespace std; int main() { std::set<int> int_set; auto f = [&int_set](){ try { std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<> dis(1, 1000); for (std::size_t i = 0; i != 100000; ++i) { int_set.insert(dis(gen)); cout << dis(gen) << endl; } } catch (...) { } }; std::thread td1(f), td2(f); td1.join(); td2.join(); getchar(); return 0; }
#include <thread> #include <set> #include <random> #include <mutex> using namespace std; int main() { std::mutex mt; std::set<int> int_set; auto f = [&int_set, &mt](){ try { std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<> dis(1, 1000); for (std::size_t i = 0; i != 100000; ++i) { mt.lock(); int_set.insert(dis(gen)); mt.unlock(); cout << dis(gen) << endl; } } catch (...) { } }; std::thread td1(f), td2(f); td1.join(); td2.join(); getchar(); return 0; }
互斥对象管理类模板的加锁策略
前面提到std::lock_guard、std::unique_lock和std::shared_lock类模板在构造时是否加锁是可选的,C++11提供了3种加锁策略。
策略 | tag type | 描述 |
---|---|---|
(默认) | 无 | 请求锁,阻塞当前线程直到成功获得锁。 |
std::defer_lock | std::defer_lock_t | 不请求锁。 |
std::try_to_lock | std::try_to_lock_t | 尝试请求锁,但不阻塞线程,锁不可用时也会立即返回。 |
std::adopt_lock | std::adopt_lock_t | 假定当前线程已经获得互斥对象的所有权,所以不再请求锁。 |
下表列出了互斥对象管理类模板对各策略的支持情况。
策略 | std::lock_guard | std::unique_lock | std::shared_lock |
---|---|---|---|
(默认) | √ | √ | √(共享) |
std::defer_lock | × | √ | √ |
std::try_to_lock | × | √ | √ |
std::adopt_lock | √ | √ | √ |
下面的代码中std::unique_lock指定了std::defer_lock。
1 2 3 4 5 |
std::mutex mt; std::unique_lock<std::mutex> lck(mt, std::defer_lock); assert(lck.owns_lock() == false); lck.lock(); assert(lck.owns_lock() == true); |
对多个互斥对象加锁
在某些情况下我们可能需要对多个互斥对象进行加锁,考虑下面的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 |
std::mutex mt1, mt2; // thread 1 { std::lock_guard<std::mutex> lck1(mt1); std::lock_guard<std::mutex> lck2(mt2); // do something } // thread 2 { std::lock_guard<std::mutex> lck2(mt2); std::lock_guard<std::mutex> lck1(mt1); // do something } |
如果线程1执行到第5行的时候恰好线程2执行到第11行。那么就会出现
- 线程1持有mt1并等待mt2
- 线程2持有mt2并等待mt1
发生死锁。
为了避免发生这类死锁,对于任意两个互斥对象,在多个线程中进行加锁时应保证其先后顺序是一致。前面的代码应修改成
1 2 3 4 5 6 7 8 9 10 11 12 13 |
std::mutex mt1, mt2; // thread 1 { std::lock_guard<std::mutex> lck1(mt1); std::lock_guard<std::mutex> lck2(mt2); // do something } // thread 2 { std::lock_guard<std::mutex> lck1(mt1); std::lock_guard<std::mutex> lck2(mt2); // do something } |
更好的做法是使用标准库中的std::lock和std::try_lock函数来对多个Lockable对象加锁。std::lock(或std::try_lock)会使用一种避免死锁的算法对多个待加锁对象进行lock操作(std::try_lock进行try_lock操作),当待加锁的对象中有不可用对象时std::lock会阻塞当前线程知道所有对象都可用(std::try_lock不会阻塞线程当有对象不可用时会释放已经加锁的其他对象并立即返回)。使用std::lock改写前面的代码,这里刻意让第6行和第13行的参数顺序不同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
std::mutex mt1, mt2; // thread 1 { std::unique_lock<std::mutex> lck1(mt1, std::defer_lock); std::unique_lock<std::mutex> lck2(mt2, std::defer_lock); std::lock(lck1, lck2); // do something } // thread 2 { std::unique_lock<std::mutex> lck1(mt1, std::defer_lock); std::unique_lock<std::mutex> lck2(mt2, std::defer_lock); std::lock(lck2, lck1); // do something } |
此外std::lock和std::try_lock还是异常安全的函数(要求待加锁的对象unlock操作不允许抛出异常),当对多个对象加锁时,其中如果有某个对象在lock或try_lock时抛出异常,std::lock或std::try_lock会捕获这个异常并将之前已经加锁的对象逐个执行unlock操作,然后重新抛出这个异常(异常中立)。并且std::lock_guard的构造函数lock_guard(mutex_type& m, std::adopt_lock_t t)也不会抛出异常。所以std::lock像下面这么用也是正确
1 2 3 |
std::lock(mt1, mt2); std::lock_guard<std::mutex> lck1(mt1, std::adopt_lock); std::lock_guard<std::mutex> lck2(mt2, std::adopt_lock); |