std::lock_guard 简介
这个类是一个互斥量的包装类,用来提供自动为互斥量上锁和解锁的功能,简化了多线程编程。
互斥类的最重要成员函数是lock()和unlock()。在进入临界区时,执行lock()加锁操作,如果这时已经被其它线程锁住,则当前线程在此排队等待。退出临界区时,执行unlock()解锁操作。更好的办法是采用”资源分配时初始化”(RAII)方法来加锁、解锁,这避免了在临界区中因为抛出异常或return等操作导致没有解锁就退出的问题。极大地简化了程序员编写mutex相关的异常处理代码。C++11的标准库中提供了std::lock_guard类模板做mutex的RAII。
std::lock_guard类的构造函数禁用拷贝构造,且禁用移动构造。std::lock_guard类除了构造函数和析构函数外没有其它成员函数。
用法如下:
#include <mutex>
std::mutex kMutex;
void function() {
// 构造时自动加锁
std::lock_guard<std::mutex> (kMutex);
// 离开局部作用域,析构函数自动完成解锁功能
}
用法非常简单,只需在保证线程安全的函数开始处加上一行代码即可,其他的都在这个类的构造函数和析构函数中自动完成。
实现 my_lock_guard
这是cheng-zhi实现的一个 lock_guard
,就是在构造和析构中完成加锁和解锁的操作,之所以会自动完成,是因为离开函数作用域会导致局部变量析构函数被调用,而我们又是手动构造了 lock_guard
,因此这两个函数都是自动被调用的。
namespace myspace {
template<typename T> class my_lock_guard {
public:
// 在 std::mutex 的定义中,下面两个函数被删除了
// mutex(const mutex&) = delete;
// mutex& operator=(const mutex&) = delete;
// 因此这里必须传递引用
my_lock_guard(T& mutex) :mutex_(mutex){
// 构造加锁
mutex_.lock();
}
~my_lock_guard() {
// 析构解锁
mutex_.unlock();
}
private:
// 不可赋值,不可拷贝
my_lock_guard(my_lock_guard const&);
my_lock_guard& operator=(my_lock_guard const&);
private:
T& mutex_;
};
};
要注意的是这个类官方定义是不可以赋值和拷贝,因此需要私有化 operator =
和 copy
这两个函数。
什么是 std::mutex ?
如果你细心可以发现,不管是 std::lock_guard
,还是my_lock_guard
,都使用了一个 std::mutex
作为构造函数的参数,这是因为我们的 lock_guard
只是一个包装类,而实际的加锁和解锁的操作都还是 std::mutex
完成的,那什么是 std::mutex
呢?
在std::lock_guard对象构造时,传入的mutex对象(即它所管理的mutex对象)会被当前线程锁住。在lock_guard对象被析构时,它所管理的mutex对象会自动解锁,不需要程序员手动调用lock和unlock对mutex进行上锁和解锁操作。lock_guard对象并不负责管理mutex对象的生命周期,lock_guard对象只是简化了mutex对象的上锁和解锁操作,方便线程对互斥量上锁,即在某个lock_guard对象的生命周期内,它所管理的锁对象会一直保持上锁状态;而lock_guard的生命周期结束之后,它所管理的锁对象会被解锁。程序员可以非常方便地使用lock_guard,而不用担心异常安全问题。
std::lock_guard在构造时只被锁定一次,并且在销毁时解锁。
基本使用方法如下:
#include <mutex>
std::mutex kMutex;
void function() {
//加锁
kMutex.lock();
//kMutex.try_lock();
//do something that is thread safe...
// 离开作用域解锁
kMutex.unlock();
}
前面都提到了锁这个概念,那么什么是锁,有啥用处?
什么是锁?
锁是用来保护共享资源(变量或者代码)不被并发访问的一种方法,它只是方法,实际的实现就是 std::mutex
等等的类了。
可以简单的理解为:
- 当前线程访问一个变量之前,将这个变量放到盒子里锁住,并且当前线程拿着钥匙。这样一来,如果有其他的线程也要访问这个变量,则必须等待当前线程将盒子解锁之后才能访问,之后其他线程在访问这个变量之前也将会再次锁住这个变量。
- 当前线程执行完后,就将该盒子解锁,这样其他的线程就可以拿到盒子的钥匙,并再次加锁访问这个变量了。
这样就保证了同一时刻只有一个线程可以访问共享资源,解决了简单的线程安全问题。
什么,你还没有遇到过线程安全问题?下面开始我的表演…
一个简单的线程安全的例子
这个例子中,主线程开启了 2 个子线程,每个子线程都修改共享的全局变量 kData
,如果没有增加必要的锁机制,那么每个子线程打印出的 kData
就可能会出错。
这里使用了 3 种不同的加锁方法来解决:
- 使用 std::lock_guard
- 使用 std::mutex 实现原生的加锁
- 使用自己的 myspace::my_lock_guard
1 | #include <iostream> |
注意:在 vs
中编译这段代码。
结果分析
为什么不加锁的结果会出错?首先线程是一种轻量级的进程,也存在调度,假设当前 CPU
使用的是基于时间片的轮转调度算法,为每个进程分配一段可执行的时间片,因此每个线程都得到一段可以执行的时间(这里只是简单概括,仔细研究其实是有点复杂的,涉及到内核线程和用户线程,这里就不多说了,不是这里讨论的重点),这就导致子线程 1 在修改并打印 kData
的时候,子线程 1 的时间片用完了,CPU
切换到子线程 2 去修改并打印 kData
,这就导致了最终的打印结果不是预先的顺序,就是这个原理,简单的理解是不难的。
可能遇到的问题:
error: no matching function for call to ‘std::lock_guardstd::mutex::lock_guard()’std::lock_guardstd::mutex (kMutex);
参考这位网友(Ben Voigt)的回答,即将std::lock_guard<std::mutex> (kMutex);
改为std::lock_guard<std::mutex> (this->kMutex);
在这里表示感谢:
If you declare your lock_guard correctly, they both work:
void Foo::bad()
{
std::lock_guard<std::mutex> x{lock_};
}
void Foo::good()
{
std::lock_guard<std::mutex> y{this->lock_};
}
Using a temporary is almost useless, because the lock gets released immediately. Correct usage is to declare a local variable with automatic lifetime.
致谢
cheng-zhi《std::lock_guard 引起的思考》(注:原网站已无法访问fengbingchun《C++11中std::lock_guard的使用》