• 操作系统:ubuntu22.04
  • IDE:Visual Studio Code
  • 编程语言:C++11

算法描述

C++11 引入了标准线程库()和同步原语(主要在 中),极大地简化了多线程编程。互斥同步是多线程编程中最基础、最重要的概念之一,用于保护共享数据,防止数据竞争(Data Race),确保线程安全。

一、为什么需要互斥同步?

当多个线程同时访问和修改同一个共享资源(如全局变量、静态变量、堆内存对象等)时,如果没有同步机制,会导致:

  • 数据竞争 (Data Race):多个线程同时读写同一内存位置,结果不可预测。
  • 脏读/脏写:一个线程读取到另一个线程写入过程中的“中间状态”。
  • 程序崩溃或逻辑错误。

✅ 互斥同步的核心思想:同一时刻,只允许一个线程访问临界区(Critical Section)。

二、C++11 中的互斥量(Mutex)

C++11 在 头文件中提供了多种互斥量类型:

类型 描述 特点
std::mutex 最基本的互斥量 不可递归,不可复制,不可移动
std::recursive_mutex 递归互斥量 同一线程可多次加锁,需对应次数解锁
std::timed_mutex 带超时的互斥量 支持 try_lock_for, try_lock_until
std::recursive_timed_mutex 递归+超时互斥量 结合上述两者特性

std::mutex 基本用法

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

std::mutex mtx; // 互斥量
int shared_data = 0;

void increment()
{
    for (int i = 0; i < 100000; ++i) 
    {
        mtx.lock();           // 加锁
        ++shared_data;        // 临界区
        mtx.unlock();         // 解锁
    }
}

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

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

    std::cout << "Result: " << shared_data << std::endl; // 正确输出 200000
    return 0;
}

问题:如果在 mtx.lock() 和 mtx.unlock() 之间发生异常,可能导致永远不释放锁(死锁)!

三、RAII 锁管理:std::lock_guard 和 std::unique_lock

为避免手动管理锁导致的问题,C++11 引入了 RAII(Resource Acquisition Is Initialization)风格的锁管理类。

✅ 1. std::lock_guard(推荐用于简单场景)

  • 构造时自动加锁。
  • 析构时自动解锁。
  • 不可手动 unlock,不可转移所有权。
  • 适用于作用域明确、无条件加锁的场景。
void increment_safe()
{
    for (int i = 0; i < 100000; ++i) 
    {
        std::lock_guard<std::mutex> lock(mtx); // 自动加锁
        ++shared_data;                         // 临界区
        // 自动解锁(离开作用域时析构)
    }
}

异常安全:即使临界区内抛出异常,lock_guard 也会在栈展开时析构并解锁。

2. std::unique_lock(更灵活)

  • 支持延迟加锁、手动解锁、条件变量配合。
  • 可转移所有权(moveable)。
  • 性能略低于 lock_guard(因为要维护锁状态)。
void flexible_lock()
{
    std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 延迟加锁

    // ... 做一些不需锁的操作 ...

    lock.lock();    // 手动加锁
    shared_data++;
    lock.unlock();  // 手动解锁

    // ... 做其他事 ...

    if (!lock.owns_lock()) 
    {
        lock.lock();
        shared_data++;
        // 自动解锁(析构时)
    }
}

常用于配合 std::condition_variable:

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void worker()
{
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return ready; }); // 自动解锁等待,唤醒后自动加锁
    // do work...
}

四、避免死锁:std::lock 和 std::adopt_lock

当需要同时锁定多个互斥量时,如果顺序不一致,容易导致死锁。
✅ 使用 std::lock 同时锁定多个互斥量(避免死锁)

std::mutex m1, m2;

void transfer(Account &from, Account &to, int amount) 
{
    std::lock(m1, m2); // 同时锁定,内部避免死锁
    std::lock_guard<std::mutex> lock1(m1, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(m2, std::adopt_lock);

    from.balance -= amount;
    to.balance += amount;
}

std::adopt_lock:告诉 lock_guard,锁已经被获取,只需在析构时释放

五、带超时的互斥量:std::timed_mutex

适用于不想无限等待锁的场景。

std::timed_mutex tmtx;

void try_lock_with_timeout()
{
    if (tmtx.try_lock_for(std::chrono::milliseconds(100))) 
    {
        // 成功获取锁
        // ... 临界区 ...
        tmtx.unlock();
    } 
    else 
    {
        std::cout << "Failed to acquire lock within 100ms.\n";
    }
}

六、递归互斥量:std::recursive_mutex

允许同一线程多次加锁(必须对应次数解锁)。

std::recursive_mutex rmtx;

void recursive_func(int depth)
{
    std::lock_guard<std::recursive_mutex> lock(rmtx);
    if (depth > 0)
     {
        recursive_func(depth - 1); // 递归调用,不会死锁
    }
}

虽然方便,但应尽量避免使用,因为它掩盖了设计问题。更好的做法是重构代码,避免需要递归加锁。

七、性能与最佳实践

互斥量类型 性能 适用场景
std::mutex + lock_guard 最高 绝大多数简单同步场景
std::unique_lock 中等 需要手动控制锁、配合条件变量
std::timed_mutex 较低 需要超时机制
std::recursive_mutex 较低 仅在无法避免递归加锁时使用

最佳实践:

  • 优先使用 std::lock_guard — 简单、安全、高效。
  • 临界区尽量小 — 减少锁持有时间,提高并发性。
  • 避免在持有锁时调用用户回调或可能阻塞的函数 — 防止死锁或性能下降。
  • 多个互斥量时,使用 std::lock 避免死锁。
  • 不要复制或移动互斥量对象 — 它们是不可复制、不可移动的。
  • 析构前确保没有线程持有锁 — 否则行为未定义。

八、常见错误

❌ 1. 忘记解锁(使用裸 mutex)

mtx.lock();
// ... 可能抛异常 ...
mtx.unlock(); // 可能永远不执行!

✅ 改用 lock_guard 或 unique_lock。

❌ 2. 在析构时仍有线程持有锁

class BadExample
{
std::mutex mtx;
public:
void do_something()
{
mtx.lock();
// 长时间操作…
} // 忘记 unlock!
~BadExample() { /* mtx 可能仍被锁住!未定义行为 */ }
};

✅ 确保锁的生命周期短于互斥量。

❌ 3. 死锁(交叉加锁)

// Thread 1:
lock(m1);
lock(m2);

// Thread 2:
lock(m2);
lock(m1); // 死锁!

✅ 使用 std::lock(m1, m2); 同时加锁。

九、完整示例:线程安全计数器

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

class ThreadSafeCounter 
{
    mutable std::mutex mtx; // mutable 允许 const 函数加锁
    int count = 0;

public:
    void increment() 
    {
        std::lock_guard<std::mutex> lock(mtx);
        ++count;
    }

    int get() const
    {
        std::lock_guard<std::mutex> lock(mtx);
        return count;
    }
};

int main() 
{
    ThreadSafeCounter counter;
    std::vector<std::thread> threads;

    for (int i = 0; i < 10; ++i) 
    {
        threads.emplace_back([&counter]()
         {
            for (int j = 0; j < 10000; ++j) 
            {
                counter.increment();
            }
        });
    }

    for (auto& t : threads) {
        t.join();
    }

    std::cout << "Final count: " << counter.get() << std::endl; // 100000
    return 0;
}

总结

概念 说明
互斥量 (Mutex) 用于保护共享资源,确保同一时刻只有一个线程访问
RAII 锁 (lock_guard, unique_lock) 自动管理锁生命周期,异常安全
死锁预防 使用 std::lock 同时锁定多个互斥量
递归锁 recursive_mutex,谨慎使用
超时锁 timed_mutex,避免无限等待
最佳实践 锁粒度小、临界区短、优先用 lock_guard
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐