问题
最近在项目总结过程中,发现项目大量使用了 std::lock_guard 这个模板类,仔细分析后发现这个类牵扯到了很多重要的计算机基础,例如:多线程,互斥,锁等等,这里便记录下来,也算是一次简单的总结。
std::lock_guard 简介
这个类是一个互斥量的包装类,用来提供自动为互斥量上锁和解锁的功能,简化了多线程编程,用法如下:
#include <mutex>
std::mutex kMutex;
void function() {
// 构造时自动加锁
std::lock_guard<std::mutex> (kMutex);
// 离开局部作用域,析构函数自动完成解锁功能
}
用法非常简单,只需在保证线程安全的函数开始处加上一行代码即可,其他的都在这个类的构造函数和析构函数中自动完成。
实现 my_lock_guard
这是自己实现的一个 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_;
};
};
什么是 std::mutex ?
如果你细心可以发现,不管是 std::lock_guard,还是my_lock_guard,都使用了一个 std::mutex 作为构造函数的参数,这是因为我们的 lock_guard 只是一个包装类,而实际的加锁和解锁的操作都还是 std::mutex 完成的,那什么是 std::mutex 呢?
std::mutex 其实是一个用于保护共享数据不会同时被多个线程访问的类,它叫做互斥量,你可以把它看作一把锁,它的基本使用方法如下:
#include <mutex>
std::mutex kMutex;
void function() {
//加锁
kMutex.lock();
//kMutex.try_lock();
//do something that is thread safe...
// 离开作用域解锁
kMutex.unlock();
}
什么是锁?
锁是用来保护共享资源(变量或者代码)不被并发访问的一种方法,它只是方法,实际的实现就是 std::mutex 等等的类了。
可以简单的理解为:
-
当前线程访问一个变量之前,将这个变量放到盒子里锁住,并且当前线程拿着钥匙。这样一来,如果有其他的线程也要访问这个变量,则必须等待当前线程将盒子解锁之后才能访问,之后其他线程在访问这个变量之前也将会再次锁住这个变量。
-
当前线程执行完后,就将该盒子解锁,这样其他的线程就可以拿到盒子的钥匙,并再次加锁访问这个变量了。
这样就保证了同一时刻只有一个线程可以访问共享资源,解决了简单的线程安全问题。
lock_guard特点
- 创建即加锁,作用域结束自动析构并解锁,无需手工解锁
- 不能中途解锁,必须等作用域结束才解锁
- 不能复制
对比unique_lock
虽然lock_guard挺好用的,但是有个很大的缺陷,在定义lock_guard的地方会调用构造函数加锁,在离开定义域的话lock_guard就会被销毁,调用析构函数解锁。这就产生了一个问题,如果这个定义域范围很大的话,那么锁的粒度就很大,很大程序上会影响效率。
lock_guard最大的缺点也是简单,没有足够的灵活度。
unique_lock 对象以独占所有权的方式( unique owership)管理 mutex 对象的上锁和解锁操作,所谓独占所有权,就是没有其他的 unique_lock 对象同时拥有某个 mutex 对象的所有权。
和 lock_guard 一样,这也是一种简单而又安全的上锁和解锁方式,尤其是在程序抛出异常后先前已被上锁的 Mutex 对象可以正确进行解锁操作,极大地简化了程序员编写与 Mutex 相关的异常处理代码。
- unique_lock可以实现延时锁,即先生成unique_lock对象,然后在有需要的地方调用lock函数,lock_guard在对象创建时就自动进行lock操作了;
- unique_lock可以在需要的地方调用unlock操作,而lock_guard只能在其对象生命周期结束后自动Unlock;