Administrator
发布于 2025-09-04 / 2 阅读
0
0

同步原语std::mutex

1.什么是std::mutex

std::mutex 是C++11引入的标准库头文件中定义的一个类,代表“互斥锁”(Mutual Exclusion)。用于保护共享数据免受多线程同时访问的主要同步原语

核心思想:互斥锁就像一个小房间(临界区)的钥匙。一次只有一个线程可以持有这把钥匙(锁)。当线程持有钥匙时,它可以进入房间访问共享数据。其他线程必须等待,直到钥匙被放回(解锁),然后它们才能竞争去拿到这把钥匙。

2.为什么需要std::mutex ?

在多线程环境中,如果多个线程不加控制地同时读写同一块共享数据,会导致数据竞争(Data Race)。数据竞争的后果是未定义行为(Undefined Behavior),可能导致程序崩溃、计算错误、数据损坏等难以预料的后果。

std::mutex 的作用就是消除数据竞争,确保任何时候最多只有一个线程可以执行被保护的代码段(临界区),从而保证数据操作的原子性和正确性。

3.核心成员函数

函数

说明

lock()

尝试获取锁。如果锁已被其他线程占用,则调用线程将被阻塞(进入睡眠状态),直到锁被释放。如果锁当前未被占用,则调用线程获得该锁。

unlock()

释放锁。将锁的状态设置为空闲,允许其他被阻塞的线程尝试获取它。

try_lock()

尝试非阻塞地获取锁。如果锁已被占用,函数立即返回 false,线程不会被阻塞。如果成功获取锁,则返回 true

重要特性

  • std::mutex 既不可复制也不可移动。

  • 同一个线程不允许对已经锁定的 std::mutex 再次调用 lock()try_lock(),这会导致未定义行为(通常是死锁)。这种锁称为非递归锁

4.基本使用方式及其风险

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

std::mutex g_mutex; // 全局互斥锁
int shared_data = 0; // 共享数据

void increment() {
    for (int i = 0; i < 100000; ++i) {
        g_mutex.lock();    // 进入临界区前加锁
        ++shared_data;     // 临界区:操作共享数据
        g_mutex.unlock();  // 离开临界区后解锁
    }
}

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

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

    std::cout << "Final value: " << shared_data << std::endl; // 总是 200000
    return 0;
}

风险:上述直接使用 lock()unlock() 的方式是不安全的。如果在临界区中发生了异常、或者程序员忘记调用 unlock(),锁将永远不会被释放,导致所有等待该锁的线程永久阻塞(死锁)

5.更安全的方式:使用 RAII 管理锁

为了解决上述问题,C++ 标准库提供了RAII(Resource Acquisition Is Initialization)风格的包装器类,来自动管理锁的生命周期。

a.std::lock_guard

std::lock_guard 是一个轻量级的 RAII 包装器。它在构造时自动锁定互斥量,在析构时(例如离开作用域时)自动解锁。即使中间发生异常,也能保证锁被释放。

适用场景:简单的临界区,整个作用域都需要加锁。99% 的情况都应该优先使用它。

void safe_increment() {
    for (int i = 0; i < 100000; ++i) {
        // 构造函数中调用 g_mutex.lock()
        std::lock_guard<std::mutex> lock(g_mutex);
        ++shared_data;
        // lock 的析构函数在作用域结束时自动调用 g_mutex.unlock()
    }
}

b.std::unique_lock()

std::unique_lockstd::lock_guard 更灵活,但开销稍大。它提供了以下额外功能:

  • 延迟加锁:构造时可以指定 std::defer_lock,不立即加锁,之后再手动调用 lock()

  • 提前解锁:可以手动调用 unlock() 在作用域结束前释放锁,减少锁的持有时间。

  • 所有权转移std::unique_lock 是可移动的,但不可复制。

  • 可以与条件变量 std::condition_variable 一起使用(这是必须的)。

void flexible_increment() {
    for (int i = 0; i < 100000; ++i) {
        std::unique_lock<std::mutex> ulock(g_mutex, std::defer_lock);
        // ... 这里可以执行一些不需要锁的操作 ...
        ulock.lock(); // 现在需要操作共享数据了,手动加锁
        ++shared_data;
        ulock.unlock(); // 可以提前解锁
        // ... 这里又可以执行一些不需要锁的操作 ...
        // ulock 析构时,如果还持有锁,会自动解锁
    }
}

适用场景:需要更灵活锁管理的复杂情况,或需要与条件变量配合时。

6.其他类型的互斥量

标准库还提供了其他几种互斥量以适应不同场景:

类型

说明

std::recursive_mutex

递归互斥量。允许同一个线程多次获取同一个锁,通常用于递归函数。需要有相同次数的 unlock() 调用。

std::timed_mutex

带超时的互斥量。提供了 try_lock_for()try_lock_until() 方法,可以尝试在指定时间内获取锁。

std::recursive_timed_mutex

带超时的递归互斥量。以上两者的结合。


评论