C++多线程:unique_lock源码分析与使用详解(六)

1、unique_lock介绍
  • unique_lock取代lock_guard,unique_lock更像lock_guard的升级版,也是mutex的管家!

  • unique_lock是个类模板,工作中一般使用lock_guard(推荐使用)

    • lock_guard取代了mutex的lock()和unlock()
  • unique_lock比lock_guard灵活很多,效率差一点,内存占用多一点

    • lock_guard额外参数只能传入std::adopt_lock
    • unique_lock额外参数可以传入std::adopt_lock、std::try_to_lock、std::defer_lock...
  • 额外参数的解释

    • std::adopt_lock:标记作用,通知lock_guard或unique_lock不需要再构造函数中lock(),标记的效果就是"假设调用方线程已经拥有了互斥的所有权(已经lock()成功了)"
    • std::try_to_lock:尝试获取锁,如果获取失败将不会阻塞在锁头上,会继续向下执行
    • std::defer_lock:初始化一个没有加锁的mutex

源码参数分析

cpp 复制代码
template<typename _Mutex>
class unique_lock {
public:
    typedef _Mutex mutex_type;

    unique_lock() noexcept
            : _M_device(0), _M_owns(false) {}

    explicit unique_lock(mutex_type &__m)
            : _M_device(std::__addressof(__m)), _M_owns(false) {
        lock();
        _M_owns = true;
    }

    unique_lock(mutex_type &__m, defer_lock_t) noexcept
            : _M_device(std::__addressof(__m)), _M_owns(false) {}

    unique_lock(mutex_type &__m, try_to_lock_t)
            : _M_device(std::__addressof(__m)), _M_owns(_M_device->try_lock()) {}

    unique_lock(mutex_type &__m, adopt_lock_t) noexcept
            : _M_device(std::__addressof(__m)), _M_owns(true) {
        // XXX calling thread owns mutex
    }

    template<typename _Clock, typename _Duration>
    unique_lock(mutex_type &__m,
                const chrono::time_point <_Clock, _Duration> &__atime)
            : _M_device(std::__addressof(__m)),
              _M_owns(_M_device->try_lock_until(__atime)) {}

    template<typename _Rep, typename _Period>
    unique_lock(mutex_type &__m,
                const chrono::duration <_Rep, _Period> &__rtime)
            : _M_device(std::__addressof(__m)),
              _M_owns(_M_device->try_lock_for(__rtime)) {}
	
    
    
    ~unique_lock() {
        if (_M_owns)
            unlock();
    }
};
  • unique_lock()构造:获取一把空锁,所有权也拿不到为false。
  • unique_lock(mutex_type &__m)构造:传入一个互斥量作为构造参数,然后加锁,持有权为true
  • unique_lock(mutex_type &__m, adopt_lock_t)构造:传入mutex和std::adopt_lock,不在加锁,但是持有权默认为true
  • unique_lock(mutex_type &__m, try_to_lock_t)构造:传入mutex和std::try_to_lock,尝试加锁,持有权为尝试加锁的结果
  • unique_lock(mutex_type &__m, defer_lock_t) 构造:传入mutex和std::defer_lock,传入一把锁但不加锁,持有权为false
  • 另外下面几个带chrono的构造表示睡眠的时间构造。
  • 析构函数很特别:析构函数里写的如果持有锁那么就释放锁,如果不持有锁那么就不释放锁。因此如果我们提前释放锁,那么它不释放

看完上面的构造源码和析构源码,那么下面使用这些参数和构造方法也是一件非常简单的事情了!

2、std::adopt_lock
cpp 复制代码
std::mutex mutex_lock;

mutex_lock.lock();
std::unique_lock<std::mutex>(mutex_lock, std::adopt_lock);

std::adopt_lock的含义是高速unique_lock不需要你再去加锁,因此需要手动去加锁,否则没有效果,如上面构造源码。

3、std::try_to_lock的使用
  • 尝试用mutex的lock()去锁定这个mutex,如果没有锁成功也会立即返回并不会阻塞在锁头上,前提是不能先lock()
  • out方法中对线程进行抱着锁睡2秒
  • 如果当执行poll_msg函数的线程1先拿到锁,那么线程2可能在队列中一个元素也不会放进去。因为一旦获取锁失败,循环将继续向下走。
  • 如果线程2先拿到锁,在一个时间片内(具体多久看操作系统的调度)会尽可能的多次获取到锁尝试放入元素。
cpp 复制代码
void inMsgRecvQueue(){
    for(int i = 0;i < n;i++){
        // 尝试获取锁
        std::unique_lock<std::mutex> uniqueLock(mutex_lock, std::try_to_lock);
        if(uniqueLock.owns_lock()){
            q.push(i);
            std::cout << "inMsgRecvQueue()执行, 插入一个元素i = " << i << std::endl;
        }
    }
}

int poll_msg(){
    int msg = -1;
    std::unique_lock<std::mutex> uniqueLock(mutex_lock, std::try_to_lock);
    std::chrono::milliseconds duration(2000);
    std::this_thread::sleep_for(duration);
    std::cout << "queue size = " << q.size() << std::endl;
    if(q.size()) {
        msg = q.front();
        q.pop();
    }
    return msg;
}
3、std::defer_lock的使用
  • 这个参数的构造函数很有趣,将unique_lock与mutex绑定在一起,但是没有加锁也没有持有权。

  • 但是我们可以通过unique_lock提供的函数去加锁、解锁、尝试锁...

cpp 复制代码
std::unique_lock<std::mutex> uniqueLock(mutex_lock, std::defer_lock);
uniqueLock.lock();							//  加锁
uniqueLock.owns_lock();				// 是否持有锁:有返回true,否则返回false
uniqueLock.unlock();					// 解锁
std::mutex *ptr = uniqueLock.release();					// 将unique_lock与mutex解除绑定,并且返回指向mutex的指针
uniqueLock.try_lock();					// 尝试获取锁,成功true,失败false
uniqueLock.mutex();						// 返回unique_lock绑定的mutex对象
  • 其实unique_lock传入defer_lock只是进行了一层封装,上面这些加锁解锁的函数mutex也有。

  • 提供这些函数可以很灵活的控制临界区的大小,也就是程序的粒度大小,粒度越细程序越快。

4、unique_lock所有权的传递

其实unique_lock中还有下面几个拷贝构造和赋值=运算符的重载

cpp 复制代码
unique_lock(const unique_lock&) = delete;
unique_lock& operator=(const unique_lock&) = delete;

unique_lock(unique_lock&& __u) noexcept : _M_device(__u._M_device), _M_owns(__u._M_owns){
        __u._M_device = 0;
        __u._M_owns = false;
}

unique_lock& operator=(unique_lock&& __u) noexcept{
        if(_M_owns)
            unlock();
        unique_lock(std::move(__u)).swap(*this);
        __u._M_device = 0;
        __u._M_owns = false;
        return *this
  • unique_lock(unique_lock&& __u) :这是一个锁的移动构造,如果传入一把锁mutex进来,然后交给当前unique_lock持有,原先的unique_lock不在持有这把锁

    cpp 复制代码
    // 1
    std::unique_lock<std::mutex> uniqueLock1(mutex_lock);
    std::unique_lock<std::mutex> uniqueLock2(std::move(uniqueLock1));
    
    
    // 2
    std::unique_lock<std::mutex> return_unique_lock(){
            std::unique_lock<std::mutex> uniqueLock(mutex_lock);
            return uniqueLock;
    }
    
    std::unique_lock<std::mutex> uniqueLock = return_unique_lock();

    最后uniqueLock2持有mutex_lock,uniqueLock1不再持有

  • unique_lock& operator=(unique_lock&& __u) :这是一个返回类型为引用的赋值函数重载,如果有可能将原先持有的解锁,然后拿到传入进来的锁,并且将当前的锁返回,返回的是一个引用,因此还可以继续链式编程,但是这个方法已经废弃...

    cpp 复制代码
    std::unique_lock<std::mutex> uniqueLock1(mutex_lock);
    std::unique_lock<std::mutex> uniqueLock2(mutex_lock);
    
    //        uniqueLock2 = uniqueLock1;
    //        uniqueLock1.operator=(uniqueLock2);
相关推荐
远望清一色2 分钟前
基于MATLAB的实现垃圾分类Matlab源码
开发语言·matlab
confiself11 分钟前
大模型系列——LLAMA-O1 复刻代码解读
java·开发语言
凌云行者21 分钟前
OpenGL入门005——使用Shader类管理着色器
c++·cmake·opengl
XiaoLeisj23 分钟前
【JavaEE初阶 — 多线程】Thread类的方法&线程生命周期
java·开发语言·java-ee
凌云行者24 分钟前
OpenGL入门006——着色器在纹理混合中的应用
c++·cmake·opengl
杜杜的man26 分钟前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*27 分钟前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
半桶水专家28 分钟前
go语言中package详解
开发语言·golang·xcode
llllinuuu29 分钟前
Go语言结构体、方法与接口
开发语言·后端·golang
cookies_s_s29 分钟前
Golang--协程和管道
开发语言·后端·golang