C++ mutex
数据竞争现象
数据竞争(data race)。在并发编程中,多个线程如果同时访问相同的数据,就会发成data race,这时候 访问就可能失败。解决这个问题的方法是给共享的资源加锁 mutex。线程A要访问共享数据的时候获取mutex, 然后上锁,那么这个共享资源就只能被线程A获取,其他线程就只有在线程A解锁之后(即交出数据访问权限) 才能访问这个共享资源。
比如:
#include <iostream> #include <thread> #include <mutex> #include <vector> std::mutex increment; void countEven(const std::vector<int>& numbers, int& numEven) { for (const auto n : numbers) { if (n % 2 == 0) { increment.lock(); numEven++; increment.unlock(); } } } int main() { ...
numEven是共享资源,其他线程可能也会访问,这时候线程在访问前increment.lock(),结束之后increment.unlock(), 这样就可以保证多个线程访问numEven而不会出现数据问题。
使用std::mutex容易出现的问题
但是在实际使用中,这种方法往往会因为一些问题导致线程无法解锁,或者由于程序员的原因忘了解锁,其他线程就无法 访问数据了。比如下面的两种情况:
unlock之前出现异常,是线程退出,但是无法解锁。
std::mutex increment; void threadA (int& numEven) { ... increment.lock(); ... //处理数据,但是发生异常退出线程。 increment.unlock(); // 由于前面异常退出,导致unlock没有执行到,不能解锁。 } int main() { ... }
重复锁定导致死锁
之前看见过一个代码,大致如下,导致了死锁:
std::mutex mutex; int s_num; void handle() { mutex.lock(); ... mutex.unlock(); } void threadA() { mutex.lock(); .... handle(); // 因为前面已经lock了,handle里面的lock就一直阻塞,导致死锁。 mutex.unlock(); }
上面两种情况或者由于程序员的疏忽,或者由于程序处理异常的原因,都会导致死锁。那么有没有什么可以 避免这种情况呢? 当然 c++11 中的 std::lock_guard 可以避免这种情况。
自动上锁和解锁 std::lock_guard
std::lock_guard 是c++11提供的一个 class ,结合 std::mutex 使用,它在实例化的时候,在构造函数中 自动上锁,上锁之后,在作用域块内一直锁住,知道离开作用域,对象被析构时,自动解锁。这样将 lock 和 unlock 绑定在对象构造函数和析构函数中,就避免了没有解锁的问题。
比如:
std::mutex mtx; void threadA (int& numEven) { ... std::lock_guard<std::mutex> lck(mtx) ... //处理数据,即使发生异常,lck实例也会析构,同时unlock。 } int main() { ... }
std::lock_guard的局限
使用 std::lock_guard 很简单,但是不太灵活。而且在使用时还会有其他的问题,这个 class,没有其 他的接口,也就是除了在构造,析构的时候 lock 和 unlock ,我们几乎不能进行其他的操作。比如:
作用域执行较长代码或者耗时较长,浪费其他线程等待的时间
std::mutex mtx; void threadA (int& numEven) { ... std::lock_guard<std::mutex> lck(mtx) numEven++; // 只有这里访问numEven,后面就不再使用。 ... // 这里需要执行比较长的代码,或者需要比较长的时间。其他线程无法获取锁。 } void threadB (int& numEven) { ... std::lock_guard<std::mutex> lck(mtx); // 如果threadA没有执行完,这里就无法往下执行。 numEven++; ... } int main() { ... }
需要使用条件锁的时候,无法解锁。
使用条件锁时:std::condition_variable::wait(...) 的时候需要 unlock ,当收到 std::condition_variable.notify_one() 信号之后需要 lock 。但是 guard_lock 不能提相应的接口实现这个功能。因此,使用条件锁的时候,根 本就不能使用 guard_lock。
如果在不需要 lock 的时候给我们提供一个 unlock 的接口那就好了。这样 threadB 就不用等待那么久了。 可以实现吗?当然,c++11 还提供了另一个 class: std::unique_lock ,这个 class 可以实现这个需求。
std::unique_lock()
unique_lock 和 lock_guard 一样都是在构造的时候自动 lock,在析构的时候自动 unlock,但不同的是他 提供了其他的接口可以在中间让我们unlock。
比如在执行较长代码中,我们可以在不需要lock的时候,调用接口unlock :
std::mutex mtx; void threadA (int& numEven) { ... std::unique_lock<std::mutex> lck(mtx) numEven++; // 只有这里访问numEven,后面就不再使用。 lck.unlock(); // 调用接口unlock。 ... lck.lock(); // 如果后面还需要lock,可以调用接口lock。 ... } void threadB (int& numEven) { } int main() { ... }
当然,在使用条件锁的时候,他会根据环境自动处理lock和unlock:
std::mutex mtx; std::condition_variable cond; bool someCheck() { if(...) return true; return false; } void threadA (int& numEven) { ... std::unique_lock<std::mutex> lck(mtx) cond.wait(lck, someCheck()); numEven++; // 只有这里访问numEven,后面就不再使用。 lck.unlock(); // 调用接口unlock。 ... lck.lock(); // 如果后面还需要lock,可以调用接口lock。 ... } void threadB (int& numEven) { std::lock_guard<std::mutex> lck(mtx); ... cond.notify_one(); } int main() { ... }
总结:std::unique_lock()和std::lock_guard()的区别
两者相同的地方是,都会在构造的时候自动lock,析构的时候自动unlock。
不同的是 lock_guard 使用简单,但是没有 lock/unlock 的接口,所以使用不灵活。而 unique_lock 则提供了这些接口。所以:
- 在执行较长时间,或者要在不需要lock的时候unlock,那就要使用 unique_lock。
- 在使用条件锁的时候,只能使用 unique_lock。