C++ `std::lock_guard` 与 `std::unique_lock`:RAII 风格的锁管理

好的,没问题。

好的,让我们开始今天的讲座,主题是“C++ std::lock_guardstd::unique_lock:RAII 风格的锁管理”。 这两个家伙是C++并发编程中管理互斥锁的利器,可以让你更安全、更方便地使用锁,避免手动加锁解锁带来的各种坑。

前言:锁,并发编程中的守护神

在并发编程的世界里,多线程就像一群熊孩子在抢玩具,如果没有人管着,那场面简直是一团糟。 锁就像一个严厉的家长,它确保每次只有一个熊孩子能拿到玩具(访问共享资源),其他熊孩子必须乖乖排队等着。 锁的存在是为了防止多个线程同时修改同一份数据,导致数据损坏或者程序行为异常。

RAII:资源获取即初始化

在深入std::lock_guardstd::unique_lock之前,我们需要了解一个重要的概念:RAII(Resource Acquisition Is Initialization,资源获取即初始化)。 RAII 是一种编程技术,它将资源的获取和释放与对象的生命周期绑定在一起。 简单来说,就是让对象在构造的时候获取资源,在析构的时候释放资源。 这样,即使程序因为异常提前退出,资源也能得到正确释放,避免资源泄漏。

在锁管理方面,RAII 意味着在创建锁对象的时候获取锁,在锁对象销毁的时候释放锁。 这样,无论程序如何执行,只要锁对象离开了作用域,锁就会自动释放,保证了程序的正确性。 std::lock_guardstd::unique_lock就是 RAII 风格的锁管理工具。

std::lock_guard:简单粗暴的锁卫士

std::lock_guard是一个非常简单的锁管理器。 它的主要功能就是在构造的时候获取锁,在析构的时候释放锁。 std::lock_guard 不提供任何额外的操作,比如手动解锁、尝试加锁等。 它就是一个简单粗暴的锁卫士,保证在作用域内始终持有锁。

代码示例:std::lock_guard 的基本用法

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

std::mutex mtx; // 声明一个互斥锁
int shared_data = 0;

void increment_data() {
    std::lock_guard<std::mutex> lock(mtx); // 创建 lock_guard 对象,并获取锁
    shared_data++;
    std::cout << "Thread ID: " << std::this_thread::get_id() << ", shared_data: " << shared_data << std::endl;
} // lock_guard 对象销毁,自动释放锁

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

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

    std::cout << "Final shared_data: " << shared_data << std::endl;

    return 0;
}

在这个例子中,我们创建了一个 std::lock_guard 对象 lock,并将互斥锁 mtx 传递给它。 在 lock 对象构造的时候,它会自动调用 mtx.lock() 获取锁。 当 increment_data 函数执行完毕,lock 对象销毁的时候,它会自动调用 mtx.unlock() 释放锁。 这样,就保证了在 increment_data 函数执行期间,只有一个线程能够访问 shared_data 变量。

std::lock_guard 的特点:

  • 简单易用: 构造即加锁,析构即解锁,无需手动管理锁的生命周期。
  • 安全性高: 基于 RAII 机制,即使发生异常也能保证锁的释放。
  • 功能有限: 不支持手动解锁、尝试加锁等操作。

std::lock_guard 适用场景:

  • 需要独占访问共享资源,且不需要手动解锁的场景。
  • 对锁的管理要求简单,不需要灵活控制锁的生命周期的场景。

std::unique_lock:灵活多变的锁管家

std::unique_lock 是一个比 std::lock_guard 更加灵活的锁管理器。 它也基于 RAII 机制,但在构造的时候可以选择不获取锁,并且提供了手动解锁、尝试加锁、延迟加锁等功能。 std::unique_lock 就像一个灵活多变的锁管家,可以根据不同的需求来管理锁的生命周期。

代码示例:std::unique_lock 的基本用法

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

std::mutex mtx; // 声明一个互斥锁
int shared_data = 0;

void increment_data() {
    std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 创建 unique_lock 对象,但不获取锁
    //... 做一些准备工作 ...
    lock.lock(); // 手动获取锁
    shared_data++;
    std::cout << "Thread ID: " << std::this_thread::get_id() << ", shared_data: " << shared_data << std::endl;
    lock.unlock(); //手动释放锁
    //... 做一些其他工作 ...
    lock.lock(); // 再次获取锁
    shared_data++;
    std::cout << "Thread ID: " << std::this_thread::get_id() << ", shared_data: " << shared_data << std::endl;
} // lock_guard 对象销毁,自动释放锁

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

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

    std::cout << "Final shared_data: " << shared_data << std::endl;

    return 0;
}

在这个例子中,我们创建了一个 std::unique_lock 对象 lock,并将互斥锁 mtxstd::defer_lock 传递给它。 std::defer_lock 表示在构造 lock 对象的时候不获取锁。 然后,我们可以手动调用 lock.lock() 获取锁,调用 lock.unlock() 释放锁。 这样,就可以灵活地控制锁的生命周期。

std::unique_lock 的特点:

  • 灵活性高: 支持手动解锁、尝试加锁、延迟加锁等操作。
  • 功能强大: 可以与条件变量一起使用,实现更复杂的线程同步。
  • 安全性高: 基于 RAII 机制,即使发生异常也能保证锁的释放。
  • 开销稍大: 相比 std::lock_guardstd::unique_lock 的开销稍微大一些。

std::unique_lock 的构造函数:

std::unique_lock 提供了多个构造函数,可以根据不同的需求来创建 unique_lock 对象。

构造函数 描述
unique_lock(mutex_type& m) 构造一个 unique_lock 对象,并获取互斥锁 m
unique_lock(mutex_type& m, std::defer_lock) 构造一个 unique_lock 对象,但不获取互斥锁 m。 需要手动调用 lock() 方法来获取锁。
unique_lock(mutex_type& m, std::try_to_lock) 构造一个 unique_lock 对象,并尝试获取互斥锁 m。 如果获取锁失败,则 owns_lock() 方法返回 false
unique_lock(mutex_type& m, std::adopt_lock) 构造一个 unique_lock 对象,并假定调用线程已经拥有互斥锁 m 的所有权。 unique_lock 对象销毁时,会自动释放互斥锁 m。 这个构造函数主要用于在已经持有锁的情况下,将锁的管理权交给 unique_lock 对象。 使用这个构造函数必须非常小心,确保调用线程确实拥有锁的所有权,否则可能会导致未定义行为。

std::unique_lock 的常用方法:

方法 描述
lock() 获取锁。 如果锁已经被其他线程持有,则当前线程会阻塞,直到获取锁为止。
unlock() 释放锁。
try_lock() 尝试获取锁。 如果获取锁成功,则返回 true;否则返回 false。 不会阻塞当前线程。
owns_lock() 检查 unique_lock 对象是否拥有锁。 如果拥有锁,则返回 true;否则返回 false
release() 释放 unique_lock 对象对锁的所有权。 返回指向互斥锁的指针。 调用 release() 方法后,unique_lock 对象不再管理锁的生命周期,需要在其他地方手动释放锁。 这个方法主要用于将锁的管理权交给其他对象或函数。

std::unique_lock 适用场景:

  • 需要灵活控制锁的生命周期的场景。
  • 需要手动解锁、尝试加锁等操作的场景。
  • 需要与条件变量一起使用的场景。

std::unique_lock 与条件变量:线程同步的黄金搭档

std::unique_lock 经常与条件变量(std::condition_variable)一起使用,实现更复杂的线程同步。 条件变量允许线程在满足特定条件时才继续执行,否则就进入等待状态。 当其他线程修改了共享数据,使得条件满足时,可以通知等待的线程继续执行。

代码示例:std::unique_lock 与条件变量的使用

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

std::mutex mtx;
std::condition_variable cv;
std::queue<int> data_queue;
bool data_ready = false;

void producer() {
    for (int i = 0; i < 10; ++i) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟生产数据
        std::unique_lock<std::mutex> lock(mtx);
        data_queue.push(i);
        data_ready = true;
        std::cout << "Producer: Produced data " << i << std::endl;
        cv.notify_one(); // 通知等待的消费者线程
    }
}

void consumer() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return data_ready; }); // 等待数据准备好
    while (!data_queue.empty()) {
        int data = data_queue.front();
        data_queue.pop();
        std::cout << "Consumer: Consumed data " << data << std::endl;
    }
    data_ready = false; // 重置 data_ready 标志
}

int main() {
    std::thread t1(producer);
    std::thread t2(consumer);

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

    return 0;
}

在这个例子中,producer 线程生产数据,并将数据放入 data_queue 中。 然后,它设置 data_ready 标志为 true,并调用 cv.notify_one() 通知等待的 consumer 线程。 consumer 线程等待 data_ready 标志为 true,然后从 data_queue 中取出数据进行消费。 这里,std::unique_lock 提供了 cv.wait() 方法需要的锁对象,保证了在等待期间锁的正确释放和获取。

std::lock_guard vs std::unique_lock:如何选择?

特性 std::lock_guard std::unique_lock
灵活性
功能 简单加锁/解锁 手动加锁/解锁,尝试加锁,延迟加锁,与条件变量一起使用
开销
适用场景 简单互斥,不需要手动控制锁的生命周期 需要灵活控制锁的生命周期,需要与条件变量一起使用
使用方法 构造即加锁,析构即解锁 构造时可以选择是否加锁,需要手动调用 lock()unlock() 方法
代码复杂度

总的来说,如果你的需求很简单,只需要在作用域内独占访问共享资源,那么 std::lock_guard 是一个不错的选择。 如果你需要更灵活地控制锁的生命周期,或者需要与条件变量一起使用,那么 std::unique_lock 更加适合你。

总结:RAII 锁管理的魅力

std::lock_guardstd::unique_lock 都采用了 RAII 机制,将锁的获取和释放与对象的生命周期绑定在一起,可以有效地避免手动加锁解锁带来的各种问题,提高程序的安全性和可靠性。 在并发编程中,选择合适的锁管理工具非常重要。 std::lock_guardstd::unique_lock 为我们提供了方便、安全、灵活的锁管理方案,让我们可以更加专注于业务逻辑的实现。

好了,今天的讲座就到这里。 希望大家能够掌握 std::lock_guardstd::unique_lock 的用法,并在实际的并发编程中灵活运用。 记住,锁是并发编程中的守护神,合理使用锁可以保护你的程序免受数据竞争的困扰。 感谢大家的聆听!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注