【C++入门到精通】Lock_guard与Unique_lock C++11 [ C++入门 ]

简介: 【C++入门到精通】Lock_guard与Unique_lock C++11 [ C++入门 ]

引言

在C++11标准中,为了更方便地使用互斥锁(Mutex)来保证多线程程序的安全性,Lock_guard和Unique_lock这两个类也被引入。它们作为RAII(资源获取即初始化)机制的一种实现方式,在多线程编程中起到了重要的作用。Lock_guard和Unique_lock可以帮助程序员自动管理互斥锁的加锁和解锁,避免忘记解锁而导致的死锁等问题。本文将详细介绍Lock_guard和Unique_lock的使用方法和区别,并通过实例展示如何使用它们来实现线程安全的程序。

一、RAII机制

1. 概念

RAII(Resource Acquisition Is Initialization)是一种C++编程技术,它通过将资源的获取和释放与对象的生命周期绑定在一起,以确保资源在对象创建时获取,在对象销毁时释放。这种技术利用了C++对象的构造函数和析构函数的调用机制,使得资源的管理变得更加简洁、安全和可靠。

2. 原理

使用RAII的关键在于将资源的获取和释放操作分别放置在对象的构造函数和析构函数中。当对象被创建时,构造函数负责获取资源并进行必要的初始化工作;当对象被销毁时,析构函数负责释放资源并进行清理工作。由于C++保证在对象销毁时析构函数会被自动调用,所以资源的释放也就得到了保证。

3. 优点

RAII技术的优点如下:

  1. 简洁性:通过对象的自动构造和析构,减少了手动管理资源的代码量,使得程序更加简洁易读。
  2. 安全性:确保资源在适当的时候被释放,避免了常见的资源泄漏和错误状态的产生。
  3. 可靠性:无论在何时何地发生异常或提前退出,都能够保证资源的正确释放,提高程序的可靠性。
  4. 可扩展性:通过继承和组合等方式,可以方便地扩展和管理更复杂的资源。

常见的使用RAII技术的例子包括使用智能指针管理动态内存、使用文件对象进行文件操作、使用互斥锁类进行线程同步等。通过合理运用RAII技术,可以有效地提高代码的可维护性、可读性和可靠性,是现代C++编程中的重要技术之一。

二、Lock_guard

1. 官方文档

⭕Lock_guard官方文档

2. 概念

std::lock_guard是C++标准库中的一个RAII类模板,用于管理互斥锁(std::mutex)的加锁和解锁操作。它提供了一种简单且安全的方式来确保在退出作用域时互斥锁会被正确地释放,从而避免了忘记解锁而导致的死锁和资源泄漏等问题

3. 底层类模版

// lock_guard类模板,用于管理互斥锁的加锁和解锁操作
template<class _Mutex>
class lock_guard
{
public:
    // 构造函数,在创建lock_guard对象时自动上锁互斥锁_Mtx
    explicit lock_guard(_Mutex& _Mtx)
        : _MyMutex(_Mtx)
    {
        _MyMutex.lock(); // 使用互斥锁的lock()成员函数进行上锁操作
    }

    // 构造函数,在已经上锁的情况下创建lock_guard对象
    // 在这种情况下,不需要再次上锁互斥锁,因此该构造函数空实现
    lock_guard(_Mutex& _Mtx, adopt_lock_t)
        : _MyMutex(_Mtx)
    {}

    // 析构函数,在lock_guard对象销毁时自动解锁互斥锁_Mtx
    // 使用互斥锁的unlock()成员函数进行解锁操作
    ~lock_guard() _NOEXCEPT
    {
        _MyMutex.unlock();
    }

    // 禁用拷贝构造函数和拷贝赋值运算符,确保lock_guard对象不可拷贝
    lock_guard(const lock_guard&) = delete;
    lock_guard& operator=(const lock_guard&) = delete;

private:
    _Mutex& _MyMutex; // 引用类型成员变量,用于保存互斥锁的引用
};

lock_guard类有以下几个重要成员函数:

  1. explicit lock_guard(_Mutex& _Mtx):该构造函数会在创建lock_guard对象时自动上锁互斥锁_Mtx。它使用互斥锁的lock()成员函数进行上锁操作。
  2. lock_guard(_Mutex& _Mtx, adopt_lock_t):该构造函数用于在已经上锁的情况下创建lock_guard对象。在这种情况下,不需要再次上锁互斥锁,因此该构造函数空实现。
  3. ~lock_guard() _NOEXCEPT:析构函数会在lock_guard对象销毁时自动解锁互斥锁_Mtx。它使用互斥锁的unlock()成员函数进行解锁操作。
  4. lock_guard(const lock_guard&) = deletelock_guard& operator=(const lock_guard&) = delete:禁用拷贝构造函数和拷贝赋值运算符,确保lock_guard对象不可拷贝。

🚨注意lock_guard禁用了拷贝构造函数和拷贝赋值运算符,意味着它不支持拷贝语义,只能通过直接创建对象来使用。这样可以避免多个lock_guard对象同时管理同一个互斥锁而导致的错误行为。

4. 使用示例

使用std::lock_guard非常简单,只需在需要加锁的代码块的开始处创建一个std::lock_guard对象,并将互斥锁作为参数传递给它的构造函数。当代码块结束时,std::lock_guard对象的析构函数会自动调用,从而触发互斥锁的解锁操作。

下面是一个示例代码,展示了如何使用std::lock_guard来管理互斥锁的加锁和解锁:

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

std::mutex mtx;  // 定义一个互斥锁

void printMessage(const std::string& message) {
    std::lock_guard<std::mutex> lock(mtx);  // 创建std::lock_guard对象并传入互斥锁

    for (int i = 0; i < 5; ++i) {
        std::cout << message << std::endl;
    }
}

int main() {
    std::thread t1(printMessage, "Hello");
    std::thread t2(printMessage, "World");

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

    return 0;
}

在上述示例中,printMessage函数通过创建一个std::lock_guard对象lock,来确保在执行打印操作之前获得互斥锁,并在函数返回时自动释放互斥锁。这样,当多个线程调用printMessage函数时,它们之间的执行将会排他性地进行,避免了数据竞争和输出混乱的问题。

总之,std::lock_guard提供了一种简单且安全的方式来管理互斥锁的加锁和解锁操作,帮助我们在使用互斥锁时避免常见的错误,并提高代码的可靠性和可维护性。

三、Unique_lock

1. 官方文档

⭕Unique_lock官方文档

2. 概念及底层

unique_lock类模板与lock_guard类似,都是以资源获取就是初始化(Resource Acquisition Is Initialization,缩写为RAII)的方式对锁进行封装。它们都采用了独占所有权的机制,即不能进行拷贝操作。在构造或移动赋值时,需要将一个Mutex对象作为参数传递给unique_lock对象,新创建的unique_lock对象会负责管理该Mutex对象的上锁和解锁操作。

对于使用unique_lock实例化的对象,当对象被创建时,会自动调用构造函数对Mutex对象进行上锁操作,确保当前线程获得互斥访问权限。而当unique_lock对象销毁时,会自动调用析构函数解锁Mutex对象,释放互斥访问权限。通过这种方式,可以方便地避免死锁问题的发生,保证线程安全性。

与lock_guard不同的是,unique_lock更加的灵活,提供了更多的成员函数:

  1. 上锁/解锁操作locktry_locktry_lock_fortry_lock_untilunlock
  2. 修改操作:移动赋值、交换(swap():与另一个unique_lock对象互换所管理的互斥量所有权)、释放(release():返回它所管理的互斥量对象的指针,并释放所有权)
  3. 获取属性owns_lock(返回当前对象是否上了锁)、operator bool()(与owns_lock()的功能相同)、mutex(返回当前unique_lock所管理的互斥量的指针)。

总的来说unique_lock类模板与lock_guard类似,都提供了一种方便且安全地管理互斥量的方法。它们通过RAII的思想,在对象的生命周期中自动管理锁的上锁和解锁操作,从而简化了编程过程,减少了出错的可能性,并提高了代码的可读性和可维护性。

3. 使用示例

当使用 unique_lock 时,一般需要搭配一个 std::mutex 来进行线程间同步。以下是一个简单的示例:

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

std::mutex mtx; // 创建一个互斥量

void work_in_critical_section() {
    std::unique_lock<std::mutex> lock(mtx); // 在进入临界区之前,使用 unique_lock 对互斥量进行上锁操作
    // 在这里执行需要互斥访问的操作
    std::cout << "Critical section is locked by this thread" << std::endl;
} // 离开作用域时,unique_lock 的析构函数会自动解锁互斥量

int main() {
    std::thread t1(work_in_critical_section);
    std::thread t2(work_in_critical_section);

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

    return 0;
}

在这个示例中,我们创建了一个互斥量 mtx,然后在 work_in_critical_section 函数中,我们使用 std::unique_lock 对 mtx 进行上锁操作。在 main 函数中,我们创建了两个线程分别执行 work_in_critical_section 函数。由于使用了 unique_lock,它会在离开作用域时自动解锁互斥量,确保线程安全。

四、总结

RAII机制是一种重要的编程范式,Lock_guardUnique_lock是C++标准库中用于资源管理的类模板。它们都基于RAII机制,能够简化资源的管理,提高代码的可读性和可维护性。同时,它们也提供了对互斥量的自动上锁和解锁操作,确保了线程安全。

温馨提示

感谢您对博主文章的关注与支持!另外,我计划在未来的更新中持续探讨与本文相关的内容,会为您带来更多关于C++以及编程技术问题的深入解析、应用案例和趣味玩法等。请继续关注博主的更新,不要错过任何精彩内容!再次感谢您的支持和关注。期待与您建立更紧密的互动,共同探索C++、算法和编程的奥秘。祝您生活愉快,排便顺畅!

目录
相关文章
|
4天前
|
C++ 存储 编译器
|
3天前
|
编译器 C++
C++入门(命名空间)
C++入门(命名空间)
|
4天前
|
C++ 编译器 程序员
C++ 从零基础到入门(3)—— 函数基础知识
C++ 从零基础到入门(3)—— 函数基础知识
|
4天前
|
C++ 存储
C++从零基础到入门(2)—— (if、switch、for、while语句)
C++从零基础到入门(2)—— (if、switch、for、while语句)
C++从零基础到入门(2)—— (if、switch、for、while语句)
|
4天前
|
编译器 C语言 C++
C++入门基础-2
C++入门基础
12 3
|
4天前
|
C语言 C++
C++入门基础-1
C++入门基础
18 1
|
4天前
|
自然语言处理 编译器 C语言
【C++】C++ 入门 — 命名空间,输入输出,函数新特性
本文章是我对C++学习的开始,很荣幸与大家一同进步。 首先我先介绍一下C++,C++是上个世纪为了解决软件危机所创立 的一项面向对象的编程语言(OOP思想)。
36 1
【C++】C++ 入门 — 命名空间,输入输出,函数新特性
|
4天前
|
存储 安全 编译器
【C++从练气到飞升】03---C++入门(三)
【C++从练气到飞升】03---C++入门(三)
|
4天前
|
存储 自然语言处理 编译器
【C++从练气到飞升】02---C++入门(二)
【C++从练气到飞升】02---C++入门(二)