C++多线程3
1 利用栈特性自动释放锁RAII
1.1 什么是RAII
RAII(Resource Acquisition Is Initialization),使用局部对象管理资源的技术称为资源获取既初始化,它的生命周期由操作系统管理,无需人工干预。为什么要引入自动释放锁,因为我们有时会因为忘记释放锁,而造成死锁或内存泄漏。我们先来手动实现,来理解一下它的处理过程
代码如下:
#include<thread>
#include<iostream>
#include <mutex>
#include <shared_mutex>
using namespace std;
class xmux {
public:
xmux(mutex& mux):mux_ (mux){
mux_.lock();
cout << "lock" << endl;
}
~xmux() {
cout << "unlock" << endl;
mux_.unlock();
}
private:
mutex& mux_;
};
static mutex mux;
void test(int i) {
xmux mu(ref(mux));
if (i == 1) {
cout << i << endl;
return;
}
else {
cout << "2" << endl;
return;
}
}
int main() {
for (int i = 0; i < 3; i++) {
thread th(test, i);
th.detach();
}
getchar();
}
运行结果:
1.2 C++11所支持的RAII管理互斥资源lock_guard;
我们先来看一下lock_guard的源码:
_EXPORT_STD template <class _Mutex>
class _NODISCARD_LOCK lock_guard { // class with destructor that unlocks a mutex
public:
using mutex_type = _Mutex;
explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { // construct and lock
_MyMutex.lock();
}
lock_guard(_Mutex& _Mtx, adopt_lock_t) : _MyMutex(_Mtx) {} // construct but don't lock
~lock_guard() noexcept {
_MyMutex.unlock();
}
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
private:
_Mutex& _MyMutex;
};
我们可以看到它是一个模板函数,意味这lock_guard这个类可以实现其他的锁,比如超时锁,共享锁等。此外它提供了两个构造函数一个实现了正常加锁,另一个不做任何操作,用以处理已经加过锁的mutex。lock_guard(const lock_guard&) = delete;和lock_guard& operator=(const lock_guard&) = delete;表示不允许锁的转移。下面是示例:
static mutex mux;
void guard(int i) {
{
lock_guard<mutex> glock(mux); //加锁
cout << i << "guard begin" << endl;
} //解锁
{
mux.lock(); //加锁
lock_guard<mutex> ulock(mux, adopt_lock);
cout << i << "guard end" << endl;
} 、、解锁
}
int main() {
for (int i = 0; i < 3; i++) {
thread th(guard, i);
th.detach();
}
getchar();
}
下面是运行结果:
1.3 C++11 unique_lock
- unique_lock 实现可移动的互斥体所有权包装器
- 支持临时释放锁unlock();
- 支持adopt_lock()
- 支持defer_lock() (延后拥有,不加锁,释放不解锁)
- 支持try_to_lock() (尝试获得互斥锁的所有权而不阻塞,获取失败退出栈区不会释放,通过ownslock()函数判断
unique_lock使用了移动构造函数,不会复制资源,从而保证了unique_lock是被独占的,下面是移动构造函数的源码:
_NODISCARD_CTOR_LOCK unique_lock(unique_lock&& _Other) noexcept : _Pmtx(_Other._Pmtx), _Owns(_Other._Owns) {
_Other._Pmtx = nullptr;
_Other._Owns = false;
}
unique_lock& operator=(unique_lock&& _Other) {
if (this != _STD addressof(_Other)) {
if (_Owns) {
_Pmtx->unlock();
}
_Pmtx = _Other._Pmtx;
_Owns = _Other._Owns;
_Other._Pmtx = nullptr;
_Other._Owns = false;
}
return *this;
}
unique_lock支持临时释放锁,我们有需要的话可以临时解锁
void unique(int i) {
unique_lock<mutex> ulock(mux);
ulock.unlock();
//临时释放锁;
ulock.lock();
}
unique_lock另外支持adopt_lock,defer_lock,try_to_lock三种参数;
adopt_lock
{
static mutex mux;
mux.lock();
unique_lock<mutex> ulock(mux,adopt_lock);
}
表示已经锁上不需要再锁,只获取锁的所有权
defer_lock
{
static mutex mux;
unique_lock<mutex> ulock(mux,defer_lock);
ulock.lock();
}
表示只拿到锁的地址,不进行加锁操作,如果有需要可以之后再加锁。
try_to_lock
ulock.lock();
{
static mutex mux;
unique_lock<mutex> ulock(mux,try_to_lock);
if (ulock.owns_lock()) {
// 表示加锁成功
}
}
try_to_lock进行尝试加锁,如果失败,不会阻塞线程,可以通过owns_lock()查询到是否加锁成功。
此外unique_lock还可以实现超时锁功能,通过owns_lock()查询是否加锁成功。unnique_lock更加灵活,容易使用。
1.4 C++ 14 shared_lock
C++14支持的可移动的共享锁
shared_lock与unique_lock的接口基本相同,需要注意使用shared_timed_mutex而不是mutex
{
static shared_timed_mutex smux;
shared_lock<shared_timed_mutex> slock(smux);
//
unique_lock<shared_timed_mutex> ulock(smux,try_to_lock);
if (!ulock.owns_lock()) {
cout << "unlocked" << endl;
}
}
结果如下:
这里涉及到的共享锁与互斥锁的关系可以去上一篇博客查看
1.5 C++17 scoped_lock()
有时我们会在多线程中对临界区加两个或多个互斥锁,可能会出现因为互相等待而死锁的情况。我们可以先来演示一下发生死锁的情况:
static mutex mux1;
static mutex mux2;
void test1() {
mux1.lock();
this_thread::sleep_for(10ms);//模拟因某些原因造成的时间差
mux2.lock();
this_thread::sleep_for(10ms);//模拟临界区代码
mux1.unlock();
mux2.unlock();
}
void test2() {
mux2.lock();
this_thread::sleep_for(10ms);//模拟因某些原因造成的时间差
mux1.lock();
this_thread::sleep_for(10ms);//模拟临界区代码
mux1.unlock();
mux2.unlock();
}
运行结果如下:
可以看到出现了死锁。进程一在等待锁2,进程二在等待锁1,两个进程因为相互等待而出现了死锁,实际应用中设备,软件等原因可能会造成不同线程运行时出现时间差,从而出现死锁,并且这种时间差不是经常出现的,这就导致了如果因为这样的原因而造成的程序错误往往是不可复现的。我们可以先不使用scoped_lock()来解决问题
void test1() {
this_thread::sleep_for(10ms);//模拟因某些原因造成的时间差
lock(mux1, mux2);
this_thread::sleep_for(10ms);//模拟临界区代码
mux1.unlock();
mux2.unlock();
}
void test2() {
lock(mux1, mux2);
this_thread::sleep_for(10ms);//模拟因某些原因造成的时间差
this_thread::sleep_for(10ms);//模拟临界区代码
mux1.unlock();
mux2.unlock();
}
这是出现正常运行。
在使用scoped_lock();
void test1() {
scoped_lock lock(mux1, mux2);
this_thread::sleep_for(10ms);//模拟因某些原因造成的时间差
this_thread::sleep_for(10ms);//模拟临界区代码
}
void test2() {
scoped_lock lock(mux1, mux2);
this_thread::sleep_for(10ms);//模拟因某些原因造成的时间差
this_thread::sleep_for(10ms);//模拟临界区代码
}
我们可以看到两个的区别在于scoped_lock 使用了RAII特性,不用主动去释放锁。