掘金 阅读 ( ) • 2024-04-16 12:26

===

转转于:https://gitcode.csdn.net/65eec6571a836825ed79d126.html?dp_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6NTUwNzI1NCwiZXhwIjoxNzEzODM3MzA0LCJpYXQiOjE3MTMyMzI1MDQsInVzZXJuYW1lIjoidTAxMzY2OTA1MCJ9.p7DNToVDupYJ3fgkr68Otqta2aV2OHOWSKPX3gFR0Hk

C++ 11 lock_guard 和 unique_lock 简介,使用以及其相应的原理实现

文章目录

前言

最近在写C++程序,需要用到 lock_guard ,并记录 C++11新特性 lock_guard 和 unique_lock 的使用。

一、简介

1.1 lock_guard

在 C++11 中,为了方便实现自动加锁和解锁的操作,提供了 lock_guard 类模板。它是一个轻量级的 RAII(资源获取即初始化)类,用于在作用域结束时自动释放互斥锁。

std::lock_guard 用于管理互斥锁的加锁和解锁操作。它的主要作用是在构造函数中获取一个互斥锁,然后在析构函数中自动释放该锁,以确保在锁保护区域的结束时正确解锁。

std::lock_guard 的作用是获取互斥量的锁,并在作用域结束时自动释放锁。这样可以避免手动管理锁的复杂性和风险,同时也可以确保在使用共享资源时不会被其他线程打断。

简单来说就是使用 std::lock_guard 让开发者使用时不用关心 std::mutex 锁的释放。

#include <iostream>
#include <mutex>
#include <thread>

std::mutex mtx; // the mutex to protect the shared resource

void thread_function()
{
    std::lock_guard<std::mutex> lock(mtx); // acquire the lock
    std::cout << "Thread " << std::this_thread::get_id() << " is accessing the shared resource." << std::endl;
    // access the shared resource here...

	//other code
}

int main()
{
    std::thread t1(thread_function);
    std::thread t2(thread_function);
    t1.join();
    t2.join();
    return 0;
}

上述的std::lock_guard在 执行完 thread_function 函数时,才自动释放该锁,但是我们只需要保护我们的数据,当我们使用 std::lock_guard 时,它的作用域应该被限制在需要保护的临界区域内。这样可以确保锁在不需要的时候及时释放,避免死锁和其他并发问题。因此我们在使用 std::lock_guard 时可以 限制其作用域:

#include <iostream>
#include <mutex>
#include <thread>

std::mutex mtx; // the mutex to protect the shared resource

void thread_function()
{
	//访问临界区域资源
	{
	    std::lock_guard<std::mutex> lock(mtx); // 在构造函数中调用 lock()
	    
	    std::cout << "Thread " << std::this_thread::get_id() << " is accessing the shared resource." << std::endl;
	    // access the shared resource here...
	}
	//离开作用域,lock_guard会调用其析构函数,会自动调用 unlock()

	//other code
}

int main()
{
    std::thread t1(thread_function);
    std::thread t2(thread_function);
    t1.join();
    t2.join();
    return 0;
}

变量在离开其作用域时被销毁,会自动调用其析构函数。

其他一个简单例子:

#include <mutex>
#include <iostream>
#include <thread>

class Counter {
public:
    Counter() : count_(0) {}

    void increment() {
        std::lock_guard<std::mutex> lock(mtx_);
        ++count_;
    }

    void decrement() {
        std::lock_guard<std::mutex> lock(mtx_);
        --count_;
    }

    int value() const {
        std::lock_guard<std::mutex> lock(mtx_);
        return count_;
    }

private:
    int count_;
    mutable std::mutex mtx_;
};

void worker(Counter& counter) {
    for (int i = 0; i < 100000; ++i) {
        counter.increment();
        counter.decrement();
    }
}

int main() {
    Counter counter;

    std::thread t1(worker, std::ref(counter));
    std::thread t2(worker, std::ref(counter));

    t1.join();
    t2.join();

    std::cout << "Final count: " << counter.value() << std::endl;

    return 0;
}

在这个例程中,我们定义了一个名为 Counter 的类,它包含一个计数器和一个互斥量。计数器的 increment、decrement 和 value 操作都使用了 std::lock_guard 来保护计数器的访问,从而避免了数据竞争等并发问题。

在 increment、decrement 和 value 操作中,我们使用 std::lock_guard 对象 lock 来锁定互斥量 mtx_。在锁定互斥量后,我们可以安全地访问计数器,并在操作完成后自动释放互斥量。

在 worker 函数中,我们使用两个线程来模拟计数器的增减过程。在每次循环中,线程会调用 increment 和 decrement 操作各一次,从而使计数器的值不变。最后,我们使用 value 操作输出计数器的最终值。

1.2 RAII

RAII(Resource Acquisition Is Initialization)是 C++ 中一种重要的编程技术,它的核心思想是将资源的获取和释放与对象的构造和析构绑定在一起,以确保在对象构造时获取资源,在对象析构时释放资源,从而避免资源泄漏和错误。

RAII 技术的基本原则是:将资源的获取和释放的代码封装在一个类的构造函数和析构函数中。在对象构造时,资源被获取并初始化;在对象析构时,资源被释放。这样,无论是正常退出还是异常退出,都可以确保资源被正确地释放,避免了资源泄漏和错误。

1.3 原理

lock_guard 是一个类模板,源码如下:

  /** @brief A simple scoped lock type.
   *
   * A lock_guard controls mutex ownership within a scope, releasing
   * ownership in the destructor.
   */
  template<typename _Mutex>
    class lock_guard
    {
    public:
      typedef _Mutex mutex_type;

      explicit lock_guard(mutex_type& __m) : _M_device(__m)
      { _M_device.lock(); }

      lock_guard(mutex_type& __m, adopt_lock_t) noexcept : _M_device(__m)
      { } // calling thread owns mutex

      ~lock_guard()
      { _M_device.unlock(); }

      lock_guard(const lock_guard&) = delete;
      lock_guard& operator=(const lock_guard&) = delete;

    private:
      mutex_type&  _M_device;
    };

(1)
std::lock_guard是 C++11 引入的一个标准库类模板,定义在 头文件中。它的模板参数 Mutex 是一个互斥锁类型,可以是 `std::mutex、std::recursive_mutex、std::timed_mutex、std::recursive_timed_mutex等类型的互斥锁。

  template<typename _Mutex>
  class lock_guard
    {
    public:
      typedef _Mutex mutex_type;

    private:
      mutex_type&  _M_device;
    }

std::lock_guard 使用模板类实现,是因为它需要支持不同类型的互斥锁对象,而不同类型的互斥锁对象有不同的实现方式和接口,因此需要使用模板类来实现 std::lock_guard。

在 C++11 中,标准库中提供了多种类型的互斥锁对象,如 std::mutex、std::recursive_mutex、std::timed_mutex、std::recursive_timed_mutex 等,这些互斥锁对象都有不同的实现方式和接口,但它们都具有 lock() 和 unlock() 方法,用于锁定和解锁互斥锁对象。因此,为了支持这些不同类型的互斥锁对象,需要使用模板类来实现 std::lock_guard,以便在构造函数和析构函数中调用 lock() 和 unlock() 方法,从而实现自动锁定和解锁互斥锁对象的功能。

使用模板类实现 std::lock_guard 还有一个好处,就是可以避免在使用时进行类型转换。因为模板类可以根据传入的参数类型自动推导出模板参数类型,从而在编译时确定模板类的具体实例化类型。这样可以避免手动进行类型转换,提高代码可读性和安全性。

总之,使用模板类实现 std::lock_guard 是为了支持不同类型的互斥锁对象,并且避免在使用时进行类型转换,提高代码的可读性和安全性。

(2)
std::lock_guard的构造函数接受一个互斥锁的引用,并自动获取锁。如果互斥锁已经被其他线程锁定,则当前线程会被阻塞,直到锁可用为止。如果获取锁时发生异常,则会立即释放锁。

explicit lock_guard(mutex_type& __m) : _M_device(__m)
  { _M_device.lock(); }

其中explicit 是一个关键字,用于修饰构造函数,它的作用是禁止隐式转换。如果一个构造函数被声明为 explicit,那么它就不能被用于隐式转换,只能被用于显式构造对象。

在 std::lock_guard 中,构造函数被声明为 explicit,这是因为 std::lock_guard 的作用是获取互斥量的锁,在构造函数中调用互斥量的 lock 成员函数获取锁。如果构造函数不加 explicit 关键字,那么在创建 std::lock_guard 对象时,可以隐式地将互斥量对象作为参数传递,这会导致在创建对象时就获取了锁,从而影响程序的正确性和性能。

(3)
std::lock_guard 的析构函数会在对象生命周期结束时自动释放锁。这意味着,在锁保护区域结束时,无论是正常结束还是异常结束,都会自动释放锁,从而避免了忘记释放锁的问题。

 ~lock_guard()
 { _M_device.unlock(); }

(4)
需要注意的是,std::lock_guard是一个非拷贝构造和非拷贝赋值构造的类,这意味着它不能被拷贝或赋值。这是因为拷贝或赋值会导致多个 std::lock_guard对象同时管理同一个互斥锁,从而可能导致死锁或其他并发问题。如果需要在多个线程之间共享锁,则应该使用 std::shared_lock。

//拷贝构造函数
lock_guard(const lock_guard&) = delete;
//拷贝赋值构造函数
lock_guard& operator=(const lock_guard&) = delete;

delete 是 C++11 中的一个关键字,它的作用是禁止某个函数的使用。在类中使用 delete 的语法,可以禁止拷贝构造函数和赋值运算符的使用,这样可以防止类对象被拷贝或赋值,避免了程序出错的风险。

在 std::lock_guard 中,我们使用了 delete 关键字来禁止拷贝构造函数和赋值运算符的使用。这是因为 std::lock_guard 的作用是获取互斥量的锁,它是一个非拷贝的类型,不能被拷贝构造或赋值。

在 std::lock_guard 中,拷贝构造函数和赋值运算符也被删除了,这样就可以避免在使用 std::lock_guard 对象时进行拷贝或赋值,保证了程序的正确性和性能。

禁止拷贝和拷贝赋值操作的原因是,lock_guard 对象的所有权是唯一的,不能被多个对象共享,否则会导致死锁等问题。因此,为了避免拷贝和拷贝赋值操作的错误使用,C++11 中将 lock_guard 的拷贝构造函数和拷贝赋值运算符设置为 delete,使其不能被调用。

delete 关键字是 C++11 中的一个重要特性,它可以帮助我们禁止某些函数的使用,从而防止程序出错和提高代码的健壮性和可维护性。在 std::lock_guard 中,使用 delete 关键字可以禁止拷贝构造函数和赋值运算符的使用,从而避免了类对象被拷贝或赋值的错误,提高了程序的正确性和性能。

二、unique_lock

2.1 简介

std::unique_lock 是 C++11 中的一个互斥量锁定器,它提供了灵活的锁定和解锁方式,支持超时等待和可中断等特性。与 std::lock_guard 不同,std::unique_lock 可以在构造函数中选择是否锁定,以及在析构函数中选择是否解锁,从而提供了更多的控制和灵活性。

unique_lock控制作用域内的互斥体所有权。互斥体的所有权可以延迟到构造之后,并且可以通过移动构造或移动赋值(move construction or move assignment)将其转移到另一个unique_lock。如果析构函数运行时拥有互斥锁,则所有权将被释放。

2.2 使用

#include <queue>
#include <mutex>
#include <condition_variable>
#include <iostream>
#include <thread>

template <typename T>
class ThreadSafeQueue {
public:
    ThreadSafeQueue() {}

    void push(const T& value) {
        std::unique_lock<std::mutex> lock(mtx_);
        queue_.push(value);
        cv_.notify_one();
    }

    void pop(T& value) {
        std::unique_lock<std::mutex> lock(mtx_);
        cv_.wait(lock, [this]() { return !queue_.empty(); });
        value = queue_.front();
        queue_.pop();
    }

private:
    std::queue<T> queue_;
    std::mutex mtx_;
    std::condition_variable cv_;
};

void producer(ThreadSafeQueue<int>& queue) {
    for (int i = 0; i < 10; ++i) {
        queue.push(i);
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

void consumer(ThreadSafeQueue<int>& queue) {
    int value;
    for (int i = 0; i < 10; ++i) {
        queue.pop(value);
        std::cout <<"Consumer " << std::this_thread::get_id() << " popped " << value << std::endl;
    }
}

int main() {
    ThreadSafeQueue<int> queue;

    std::thread t1(producer, std::ref(queue));
    std::thread t2(consumer, std::ref(queue));
    std::thread t3(consumer, std::ref(queue));

    t1.join();
    t2.join();
    t3.join();

    return 0;
}

在这个例程中,我们定义了一个名为 ThreadSafeQueue 的模板类,它使用 std::queue、std::mutex 和 std::condition_variable 来实现线程安全的队列。队列的 push 和 pop 操作都使用了 std::unique_lock 来保护共享资源的访问,从而避免了数据竞争等并发问题。

在 push 操作中,我们首先创建了一个 std::unique_lock 对象 lock,然后使用 lock 对象锁定互斥量 mtx_。在锁定互斥量后,我们向队列中添加一个元素,并使用条件变量 cv_ 通知等待的线程。在 pop操作中,我们也首先创建了一个 std::unique_lock 对象 lock,然后使用 lock 对象锁定互斥量 mtx_。接着,我们使用条件变量 cv_ 等待队列中有元素可供消费,如果队列为空,则线程会被阻塞,直到队列中有元素可供消费为止。一旦等待的条件得到满足,我们从队列中取出一个元素,并将其赋值给 value 参数,然后将这个元素从队列中弹出。

在 main 函数中,我们创建了一个 ThreadSafeQueue 对象 queue,并使用两个消费者线程和一个生产者线程来模拟队列的生产和消费过程。在每次循环中,生产者线程向队列中添加一个元素,并休眠 100 毫秒。消费者线程则从队列中取出元素,并输出取出的元素信息。

这个例程展示了如何使用 std::unique_lock 来保护共享资源的访问,实现线程安全的队列。通过使用 std::unique_lock,我们可以更加灵活地控制互斥量的访问,避免了数据竞争等并发问题。同时,这个例程还展示了如何使用条件变量来实现线程之间的同步,从而实现更加高效和健壮的多线程程序。

其中:

 void push(const T& value) {
     std::unique_lock<std::mutex> lock(mtx_);
     queue_.push(value);
     cv_.notify_one();
 }

这段代码实现了一个线程安全的队列中的 push 操作,它的具体含义如下:

(1)创建一个 std::unique_lock 对象 lock,并使用 mtx_ 互斥量对其进行锁定。
(2)向队列 queue_ 中添加一个元素 value。
(3)使用条件变量 cv_ 的 notify_one 函数通知等待的线程。
(4)在 push 操作完成后,std::unique_lock 对象 lock 会自动释放互斥量 mtx_。

这个 push 操作的目的是向队列中添加一个元素,并通知等待的线程。由于队列是共享资源,多个线程可能同时尝试向队列中添加元素,因此我们需要使用互斥量 mtx_ 对其进行保护,避免数据竞争等并发问题。当一个线程调用 push 操作时,它会创建一个 std::unique_lock 对象 lock,并使用互斥量 mtx_ 将其锁定。这样,其他线程就无法同时访问队列,从而保证了线程安全。

在向队列中添加元素后,我们使用条件变量 cv_ 的 notify_one 函数通知等待的线程。这样,如果有线程在等待队列中有元素可供消费,它就会被唤醒,并尝试获取互斥量 mtx_,从而继续执行消费操作。

最后,当 push 操作完成后,std::unique_lock 对象 lock 会自动释放互斥量 mtx_。这样,其他线程就可以继续访问队列,从而实现了线程安全的队列。

其中:

 void pop(T& value) {
     std::unique_lock<std::mutex> lock(mtx_);
     cv_.wait(lock, [this]() { return !queue_.empty(); });
     value = queue_.front();
     queue_.pop();
 }

这段代码实现了一个线程安全的队列中的 pop 操作,它的具体含义如下:

(1)创建一个 std::unique_lock 对象 lock,并使用 mtx_ 互斥量对其进行锁定。
(2)使用条件变量 cv_ 的 wait 函数等待队列中有元素可供消费。在等待期间,如果队列为空,则线程会被阻塞。
(3)一旦等待的条件得到满足(即队列中有元素可供消费),则从队列 queue_ 中取出一个元素,并将其赋值给 value 参数。
(4)将这个元素从队列中弹出。
(5)在 pop 操作完成后,std::unique_lock 对象 lock 会自动释放互斥量 mtx_。

这个 pop 操作的目的是从队列中取出一个元素。由于队列是共享资源,多个线程可能同时尝试从队列中取出元素,因此我们需要使用互斥量 mtx_ 对其进行保护,避免数据竞争等并发问题。当一个线程调用 pop 操作时,它会创建一个 std::unique_lock 对象 lock,并使用互斥量 mtx_ 将其锁定。这样,其他线程就无法同时访问队列,从而保证了线程安全。

在锁定互斥量后,我们使用条件变量 cv_ 的 wait 函数等待队列中有元素可供消费。在等待期间,如果队列为空,则线程会被阻塞,等待其他线程向队列中添加元素并通知等待的线程。如果队列不为空,则线程会继续执行后续操作。

备注:std::unique_lock提供了一种便利的方式来实现条件变量的同步操作,即通过 std::condition_variable 的 wait() 方法来释放锁并等待条件变量满足,然后重新获取锁并继续执行。这种方式通常用于需要等待某个条件变量满足的情况,可以避免在等待过程中一直占用锁资源,从而提高了并发性能。

一旦等待的条件得到满足,即队列中有元素可供消费,我们从队列中取出一个元素,并将其赋值给 value 参数。这样,消费者线程就可以获取队列中的元素,并进行后续的处理操作。

最后,我们将这个元素从队列中弹出,并在 pop 操作完成后,std::unique_lock 对象 lock 会自动释放互斥量 mtx_。这样,其他线程就可以继续访问队列,从而实现了线程安全的队列。

对于:

 std::unique_lock<std::mutex> lock(mtx_);
 cv_.wait(lock, [this]() { return !queue_.empty(); });

这段代码是使用条件变量 cv_ 等待队列 queue_ 不为空的操作:

(1)等待前先获取一个 std::unique_lock std::mutex 类型的锁 lock,该锁与互斥量 mutex_ 相关联,用于保证在等待条件变量时对队列 queue_ 的访问是线程安全的。

(2)调用条件变量 cv_ 的 wait() 方法,该方法会释放锁 lock 并等待条件变量满足,此时线程会处于阻塞状态。

(3)在等待过程中,条件变量 cv_ 会自动释放锁 lock,以便其他线程可以对队列 queue_ 进行访问,从而提高并发性能。

(4)等待条件变量满足后,线程会重新获取锁 lock,并继续执行后续操作。

(5)等待条件变量的满足条件是通过一个 Lambda 表达式 this { return !queue_.empty(); } 来指定的。该 Lambda 表达式的作用是检查队列 queue_ 是否为空,如果不为空则返回 true,表示等待条件变量的满足条件已经满足,否则返回 false,表示继续等待条件变量的满足。

需要注意的是,Lambda 表达式中使用了成员变量 queue_ 和 this 指针,这是因为它是在类的成员函数中使用的,通过使用 this 指针可以访问到类的成员变量和方法。

总之,这段代码的作用是实现了一种等待队列不为空的同步操作,通过条件变量 cv_ 来实现等待和唤醒线程的操作,从而避免了忙等的情况,提高了程序的效率

2.3 原理

std::unique_lock 是 C++11 引入的一种互斥锁封装,它提供了一种更加灵活的方式来管理锁的生命周期和操作。

在使用 std::unique_lock 时,需要传入一个互斥锁对象,它可以是 std::mutex、std::recursive_mutex 或其他支持 Lockable 概念的对象。std::unique_lock 对象会管理这个锁对象的锁定和释放,并且提供了许多方法来控制锁的行为。

std::unique_lock 的原理是基于 RAII(Resource Acquisition Is Initialization) 设计模式来实现的。当一个 std::unique_lock 对象被创建时,它会在构造函数中尝试锁定与之关联的互斥锁对象。当 std::unique_lock 对象被销毁时,它会在析构函数中自动释放它所持有的锁。这种自动化的锁的管理方式,可以避免在代码中手动管理锁的生命周期,从而减少了出错的风险。

此外,std::unique_lock 还提供了一些控制锁行为的方法,例如:

(1)lock():手动锁定互斥锁对象;
(2)unlock():手动释放互斥锁对象;
(3)try_lock():尝试锁定互斥锁对象,如果锁已经被其他线程持有,则返回 false。

通过这些方法,可以更加灵活地控制锁的行为,从而实现更加复杂的同步操作。

总之,std::unique_lock 的原理是基于 RAII 设计模式和互斥锁对象的锁定和释放机制来实现的,它提供了一种更加灵活和安全的方式来管理锁的生命周期和操作。

  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()
      {
	if (_M_owns)
	  unlock();
      }	

      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;
      }

    private:
      mutex_type*	_M_device;
      bool		_M_owns;
    };

其中 _M_owns 是 unique_lock 类的成员变量,表示当前 unique_lock 对象是否拥有互斥锁对象的所有权。

(1)

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

这段代码是 std::unique_lock 的构造函数的实现代码,用于创建一个 unique_lock 对象并锁定给定的互斥锁对象。其中:

explicit 关键字表示该构造函数是显式构造函数,只能用于显式地创建 unique_lock 对象。

mutex_type& __m 表示传入的互斥锁对象的引用。

_M_device(std::__addressof(__m)) 表示将传入的互斥锁对象的地址存储在 _M_device 成员变量中,
std::__addressof 是一个帮助函数,用于获取对象的地址。

lock() 是 unique_lock 类的成员函数,用于锁定互斥锁对象。

_M_owns(false) 表示初始化 _M_owns 成员变量为 false,表示当前 unique_lock 对象未拥有互斥锁对象的所有权。

lock() 成功后,将 _M_owns 成员变量设置为 true,表示当前 unique_lock 对象拥有互斥锁对象的所有权。

(2)

     ~unique_lock()
     {
if (_M_owns)
  unlock();
     }

如果当前 unique_lock 对象拥有互斥锁对象的所有权(即 _M_owns 为 true),则在 unique_lock 对象被销毁时自动执行解锁操作,将互斥锁对象解锁。这是 RAII 技术的应用,保证在离开 unique_lock 对象的作用域时自动解锁互斥锁对象,避免了忘记手动解锁的错误。

(3)

      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;
      }

这段代码是 std::unique_lock 的移动构造函数和移动赋值运算符的实现代码,用于实现 unique_lock 对象的移动语义,即将一个 unique_lock 对象的所有权转移给另一个 unique_lock 对象,而不是进行拷贝和拷贝赋值操作。

禁止拷贝和拷贝赋值操作的原因是,unique_lock 对象的所有权是唯一的,不能被多个对象共享,否则会导致死锁等问题。因此,为了避免拷贝和拷贝赋值操作的错误使用,C++11 中将 unique_lock 的拷贝构造函数和拷贝赋值运算符设置为delete,使其不能被调用。如果需要将一个 unique_lock 对象传递给另一个函数或对象,可以使用移动构造函数和移动赋值运算符来转移所有权,而不是拷贝或拷贝赋值操作。

unique_lock(unique_lock&& __u) noexcept 表示移动构造函数,用于将一个右值引用的 unique_lock 对象的所有权转移给新创建的 unique_lock 对象。具体来说,这个移动构造函数的实现是:

__u 是一个右值引用的 unique_lock 对象,表示要移动的对象。

noexcept 关键字表示该函数不会抛出异常。

_M_device(__u._M_device) 表示将要移动的对象的 _M_device 成员变量存储在新创建的对象的 _M_device 成员变量中,实现了所有权的转移。

_M_owns(__u._M_owns) 表示将要移动的对象的 _M_owns 成员变量存储在新创建的对象的 _M_owns 成员变量中,实现了所有权的转移。

__u._M_device = 0; 和 __u._M_owns = false; 表示将要移动的对象的 _M_device 和 _M_owns 成员变量设置为默认值,以避免在移动完成后执行解锁操作。

unique_lock& operator=(unique_lock&& __u) noexcept 表示移动赋值运算符,用于将一个右值引用的 unique_lock 对象的所有权转移给当前的 unique_lock 对象。具体来说,这个移动赋值运算符的实现是:

__u 是一个右值引用的 unique_lock 对象,表示要移动的对象。

if(_M_owns) unlock(); 表示如果当前 unique_lock 对象拥有互斥锁对象的所有权,则执行解锁操作,避免在移动完成后出现死锁的情况。

unique_lock(std::move(__u)).swap(*this); 表示创建一个临时的 unique_lock 对象,将要移动的对象的所有权转移给该对象,然后通过 swap() 方法将该对象的所有权转移给当前的 unique_lock 对象,实现了所有权的转移。

__u._M_device = 0; 和 __u._M_owns = false; 表示将要移动的对象的 _M_device 和 _M_owns 成员变量设置为默认值,以避免在移动完成后执行解锁操作。

return *this; 表示返回当前的 unique_lock 对象的引用,用于支持链式调用。

这段代码的意思是,为实现 unique_lock 对象的移动语义,禁止拷贝和拷贝赋值操作,创建了移动构造函数和移动赋值运算符。移动构造函数将一个右值引用的 unique_lock 对象的所有权转移给新创建的对象,移动赋值运算符将一个右值引用的 unique_lock 对象的所有权转移给当前的 unique_lock 对象。通过移动语义,可以避免进行拷贝和拷贝赋值操作,提高程序的性能。

三、 lock_guard 和 unique_lock比较

std::lock_guard 和 std::unique_lock 都是用于管理互斥锁的 C++11 标准库类,它们的主要区别在于锁的管理方式和灵活性。

3.1 锁的管理方式

std::lock_guard 是一种简单的锁管理器,它的作用是在构造函数中自动锁定互斥锁对象,并在析构函数中自动释放锁。由于 std::lock_guard 的锁定和释放是在构造函数和析构函数中完成的,因此它遵循 RAII 设计模式,可以避免在代码中手动管理锁的生命周期。

std::unique_lock 也是一种锁管理器,但相比 std::lock_guard,它提供了更加灵活的锁定和释放方式。std::unique_lock 的构造函数可以接受一个 std::defer_lock 参数,用于创建一个未锁定的 std::unique_lock 对象,而锁定操作则需要手动调用 lock() 方法来完成。此外,std::unique_lock 还提供了一些其他的特性,例如:

(1)可以在构造函数中传入一个 std::adopt_lock_t 参数,用于接管已经锁定的互斥锁对象的所有权。
(2)可以随时手动释放锁,并在需要时重新获取锁。
(3)可以通过 try_lock() 方法尝试锁定互斥锁对象,如果锁已经被其他线程持有,则返回 false。

3.2 灵活性

由于 std::unique_lock 提供了更加灵活的锁定和释放方式,因此它比 std::lock_guard 更加灵活和适用于需要进行复杂同步操作的情况。例如,在需要等待某个条件变量满足时,可以使用 std::unique_lock 结合条件变量来实现等待操作,从而避免了忙等的情况,提高了程序的效率。

此外,由于 std::unique_lock 提供了更加灵活的锁定和释放方式,因此它的性能可能会比 std::lock_guard 稍微差一些。如果只需要一个简单的锁管理器来保护共享资源,而不需要进行复杂的同步操作,那么使用 std::lock_guard 可能更加合适。

总之,std::lock_guard 和 std::unique_lock 都是用于管理互斥锁的 C++11 标准库类,它们的主要区别在于锁的管理方式和灵活性。在需要进行复杂同步操作时,建议使用 std::unique_lock。在只需要一个简单的锁管理器时,可以使用 std::lock_guard 来简化代码。

3.3 可移动性

可移动性:std::unique_lock 对象是可移动的,而 std::lock_guard 则不是。这意味着,可以使用 std::unique_lock 对象进行移动语义,从而避免了多余的互斥量锁定和解锁操作。而 std::lock_guard 则不支持移动语义,只能使用拷贝语义,这样可能会导致不必要的互斥量锁定和解锁操作。

C++11 标准中,std::lock_guard 并没有实现移动构造函数和移动赋值运算符。因为 std::lock_guard 的作用是在构造函数中自动锁定互斥锁对象,在析构函数中自动释放锁,因此它的实现方式不适合支持移动语义。如果支持移动语义,就可能导致移动后原来的对象不再具有锁定互斥锁的能力,或者移动后两个对象都具有锁定同一个互斥锁的能力,这都会导致不安全的多线程行为。

如果需要支持移动语义,可以使用 std::unique_lock。std::unique_lock 提供了更加灵活的锁定和释放方式,并支持移动语义。可以使用 std::move() 函数将一个 std::unique_lock 对象移动到另一个对象中,如下所示:

std::mutex mutex;
std::unique_lock<std::mutex> lock1(mutex);
std::unique_lock<std::mutex> lock2(std::move(lock1)); // 移动 lock1 到 lock2

需要注意的是,在使用移动语义时,需要确保原来的 std::unique_lock 对象不再使用互斥锁对象,否则可能会导致不确定的行为。由于 std::unique_lock 对象可以手动释放和重新获取锁,因此在使用移动语义时需要特别注意锁的状态和所有权的转移。

总结

以上例程参考于 Chatgpt ,Chatgpt有一些的描述会有一些错误,最好实践例程,并参考源码进行一定的对比。