C++多线程——mutex、unique_lock、condition_variable

发布时间 2023-10-30 04:41:31作者: vLiion

互斥锁 std::mutex

当涉及到多线程编程时,互斥锁(Mutex)是一种同步机制,用于保护共享资源免受并发访问的影响。以下是一个简单的C++互斥锁示例:

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

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

void printNumbers(int n) {
    for (int i = 0; i < n; ++i) {
        mtx.lock(); // 上锁
        std::cout << i << " ";
        mtx.unlock(); // 解锁
    }
}

int main() {
    const int numThreads = 3;
    const int numIterations = 5;

    std::thread threads[numThreads];
    for (int i = 0; i < numThreads; ++i) {
        threads[i] = std::thread(printNumbers, numIterations);
    }

    for (int i = 0; i < numThreads; ++i) {
        threads[i].join();
    }

    return 0;
}

在上面的示例中,我们创建了一个互斥锁 mtxprintNumbers 函数打印一些数字,并在每次打印之前上锁,然后在打印完成后解锁。这确保了每个线程都能按顺序打印数字,而不会产生竞态条件。请注意,使用互斥锁时要小心避免死锁(Deadlock)。死锁发生在两个或多个线程相互等待对方释放锁的情况下。


死锁 deadlock


这里举例给出死锁的例子

考虑以下场景:有两个线程A和B,它们都试图获取两个互斥锁(Mutex),但它们的获取顺序不一致。这可能导致死锁,因为A锁住了第一个互斥锁并等待第二个,而B锁住了第二个互斥锁并等待第一个。

以下是一个示例代码,演示了这种死锁情况:

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

std::mutex mutex1;
std::mutex mutex2;

void ThreadA() {
    std::unique_lock<std::mutex> lock1(mutex1);
    std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟一些工作
    std::cout << "Thread A 已经锁住 mutex1,尝试获取 mutex2...\n";
    
    std::unique_lock<std::mutex> lock2(mutex2); // 试图获取 mutex2
    std::cout << "Thread A 成功获取 mutex2!\n";
    // 在这里执行一些工作...
}

void ThreadB() {
    std::unique_lock<std::mutex> lock2(mutex2);
    std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟一些工作
    std::cout << "Thread B 已经锁住 mutex2,尝试获取 mutex1...\n";
    
    std::unique_lock<std::mutex> lock1(mutex1); // 试图获取 mutex1
    std::cout << "Thread B 成功获取 mutex1!\n";
    // 在这里执行一些工作...
}

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

在这个示例中,ThreadA尝试获取mutex1,而ThreadB尝试获取mutex2,它们的获取顺序不一致,因此可能导致死锁。当你运行这个程序时,你会看到两个线程都被锁住,并且程序不会继续执行下去。


std::lock_guardstd::unique_lock


lock_guard

std::lock_guard 是 C++ 标准库中的一种互斥量封装类,用于保护共享数据,防止多个线程同时访问同一资源而导致的数据竞争问题。

std::lock_guard 的特点如下:

  • 当构造函数被调用时,该互斥量会被自动锁定。

  • 当析构函数被调用时,该互斥量会被自动解锁。

  • std::lock_guard 对象不能复制或移动,因此它只能在局部作用域中使用。

std::unique_lock

std::unique_lock 是 C++ 标准库中提供的一个互斥量封装类,用于在多线程程序中对互斥量进行加锁和解锁操作。它的主要特点是可以对互斥量进行更加灵活的管理,包括延迟加锁、条件变量、超时等。

std::unique_lock 提供了以下几个成员函数:

  • lock():尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁。

  • try_lock():尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则函数立即返回 false,否则返回 true。

  • try_lock_for(const std::chrono::duration<Rep, Period>& rel_time):尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间。

  • try_lock_until(const std::chrono::time_point<Clock, Duration>& abs_time):尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间点。

  • unlock():对互斥量进行解锁操作。

除了上述成员函数外,std::unique_lock 还提供了以下几个构造函数:

  • unique_lock() noexcept = default:默认构造函数,创建一个未关联任何互斥量的 std::unique_lock 对象。

  • explicit unique_lock(mutex_type& m):构造函数,使用给定的互斥量 m 进行初始化,并对该互斥量进行加锁操作。

  • unique_lock(mutex_type& m, defer_lock_t) noexcept:构造函数,使用给定的互斥量 m 进行初始化,但不对该互斥量进行加锁操作。

  • unique_lock(mutex_type& m, try_to_lock_t) noexcept:构造函数,使用给定的互斥量 m 进行初始化,并尝试对该互斥量进行加锁操作。如果加锁失败,则创建的 std::unique_lock 对象不与任何互斥量关联。

  • unique_lock(mutex_type& m, adopt_lock_t) noexcept:构造函数,使用给定的互斥量 m 进行初始化,并假设该互斥量已经被当前线程成功加锁。


条件变量 condition_variable


条件变量具有“通知”的作用,利用变量的值为true或false,可以达到通知的作用。以下是使用条件变量来实现简单的生产者消费者模型。

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

std::queue<int> queue;
std::mutex mtx;
std::condition_variable cv;

void Producer() {
    for (int i = 0; i < 10; ++i) {
        {
            std::unique_lock<std::mutex> lock(mtx);
            queue.push(i);
            cv.notify_one();
            std::cout << "Producer : " << i << std::endl;
        }
        std::this_thread::sleep_for(std::chrono::microseconds (1000));
    }
}

void Customer() {
    while (1) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [] () {
            return !queue.empty();
        });
        int res = queue.front();
        queue.pop();

        std::cout << "Customer : " << res << std::endl;
    }
}

int main() {

    std::thread t1(Producer);
    std::thread t2(Customer);

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

    return 0;
}

完成这个程序题需要的合理的思考步骤:

  • 首先实现生产者和消费者基本逻辑

  • 由于两者共享一个队列,可能存在数据竞态的问题,于是需要加锁 mutex,用 unique_lock 来实现。

  • 队列中为空时,消费者不能取出数据,这一条件可实现为:
    消费者在准备消费前判断队列是否为空,如果是空,就等着;如果不为空,就消费。
    如果直接使用while循环进行empty判空,仍然可行,但无疑会耗费CPU资源。
    于是,我们使用条件变量来完成,通过消费者生产一个产品后,就通知消费者消费;只要生产者不通知,消费者就不会继续进行(在第一次检测队列发现为空后);