C++ 中的多线程同步:从互斥锁到读写锁

在 C++17 之前,多线程同步主要依赖于 std::mutexstd::recursive_mutexstd::timed_mutex 等互斥锁类型。随着 C++20 的到来,标准库进一步扩展了同步原语,如 std::shared_mutexstd::shared_lock 等,用于实现更细粒度的读写同步。本文将从基本概念出发,逐步剖析不同同步工具的适用场景、实现细节以及性能注意事项。

1. 互斥锁(Mutex)

1.1 基础用法

#include <mutex>
#include <thread>

std::mutex mtx;
int counter = 0;

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

std::lock_guard 是一种 RAII 风格的互斥锁包装器,构造时锁定,析构时解锁。它的优势在于异常安全:无论 increment 里出现何种异常,锁都会被正确释放。

1.2 递归互斥锁

std::recursive_mutex rmtx;

void recursive_function(int depth)
{
    std::lock_guard<std::recursive_mutex> lock(rmtx);
    if (depth > 0) recursive_function(depth - 1);
}

递归互斥锁允许同一线程多次锁定同一把锁,但实现代价更高,使用时需谨慎。

1.3 计时互斥锁

std::timed_mutex tmtx;
if (tmtx.try_lock_for(std::chrono::milliseconds(100))) {
    // 成功获取锁
    tmtx.unlock();
}

计时互斥锁支持超时机制,适合需要避免死锁的场景。

2. 读写锁(Shared Mutex)

在读多写少的场景下,使用 std::shared_mutex 可以显著提升并发性能。读者可同时获取共享锁,而写者必须独占锁。

#include <shared_mutex>
#include <vector>

std::shared_mutex rw_mutex;
std::vector <int> data;

void reader()
{
    std::shared_lock<std::shared_mutex> lock(rw_mutex);
    // 只读访问
    auto sum = std::accumulate(data.begin(), data.end(), 0);
}

void writer()
{
    std::unique_lock<std::shared_mutex> lock(rw_mutex);
    // 写操作
    data.push_back(1);
}

2.1 读写锁的竞争模型

  • 读者占用锁:只需要共享锁即可,不会阻塞其他读者。
  • 写者占用锁:独占锁会阻塞所有正在进行的读写操作,直到写者完成。

若读写比例极度偏向写,使用读写锁可能导致写者饥饿。为此,C++20 引入了 std::shared_timed_mutex,提供更灵活的等待策略。

3. 原子操作(Atomic)

C++ 标准库提供 std::atomic 用于实现无锁并发。其主要优势是原子性与轻量级。

#include <atomic>

std::atomic <int> atomic_counter{0};

void fast_increment()
{
    atomic_counter.fetch_add(1, std::memory_order_relaxed);
}
  • memory_order_relaxed 适用于不需要同步其他内存操作的情况。
  • 对于更严格的同步需求,可使用 memory_order_acquire/memory_order_release 等。

4. 条件变量(Condition Variable)

条件变量用于线程间的信号机制,常与互斥锁配合使用。

#include <condition_variable>

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

void waiter()
{
    std::unique_lock<std::mutex> lock(cv_mtx);
    cv.wait(lock, []{ return ready; });
    // 继续执行
}

void notifier()
{
    {
        std::lock_guard<std::mutex> lock(cv_mtx);
        ready = true;
    }
    cv.notify_one(); // 或 notify_all()
}

wait 的谓词形式可避免虚假唤醒。

5. 现代 C++ 并发编程技巧

技巧 说明 示例
std::scoped_lock 同时锁定多个互斥锁,避免死锁 std::scoped_lock lock(m1, m2);
std::async 异步执行返回 std::future auto fut = std::async(std::launch::async, func);
std::latch / std::barrier C++20 提供的同步原语 std::latch l(3);

6. 性能注意事项

  1. 锁粒度:过细的锁会导致频繁上下文切换,过粗的锁会降低并发度。建议按功能拆分最小共享区域。
  2. 锁竞争:通过 std::atomic 或无锁设计减少锁竞争,特别是在高并发读写场景。
  3. 缓存行对齐:避免“false sharing”,可使用 alignas(64)[[no_unique_address]]
  4. 优先级反转:在实时系统中需使用带优先级继承的互斥锁。

7. 结语

C++ 标准库为多线程同步提供了丰富且易用的原语。掌握互斥锁、读写锁、原子操作和条件变量的使用原则,并结合现代 C++ 的 RAII、std::scoped_lock 等特性,可写出既安全又高效的并发代码。在实际项目中,合理评估线程数、锁粒度与竞争情况,往往是性能优化的关键。祝你编码愉快!

发表评论