C++中如何安全地实现多线程共享内存访问?

在 C++11 及以后版本中,标准库提供了一套完整的多线程同步原语,帮助程序员在多线程环境中安全地访问共享内存。下面将从概念、常用同步工具以及最佳实践三方面进行阐述,并给出实用的代码示例。


1. 同步的核心概念

术语 含义
临界区 访问共享资源的代码段
互斥量 (mutex) 用于保证同一时刻只有一个线程进入临界区
条件变量 (condition_variable) 用于线程之间的等待与通知
原子类型 (std::atomic) 对基本类型提供无锁的原子操作
读写锁 (std::shared_mutex) 允许多个读者并发、但写者独占

理解这些概念后,才能正确选择合适的同步工具。


2. 互斥量与 lock_guard

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

std::mutex g_mutex;
int g_counter = 0;

void increment(int id, int times) {
    for (int i = 0; i < times; ++i) {
        std::lock_guard<std::mutex> lock(g_mutex);  // RAII 自动上锁/解锁
        ++g_counter;
        std::cout << "Thread " << id << " incremented counter to " << g_counter << '\n';
    }
}

int main() {
    std::vector<std::thread> workers;
    for (int i = 0; i < 4; ++i) {
        workers.emplace_back(increment, i, 5);
    }
    for (auto &t : workers) t.join();
    std::cout << "Final counter: " << g_counter << '\n';
}

要点

  • std::lock_guard 采用 RAII,异常安全。
  • 只在需要临界区时使用,避免锁持有时间过长。

3. 原子操作

对于单一整数、布尔值等基本类型,使用 std::atomic 能避免锁开销。

#include <atomic>
#include <thread>

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

void add_to_atomic(int times) {
    for (int i = 0; i < times; ++i) {
        atom_counter.fetch_add(1, std::memory_order_relaxed);
    }
}

使用建议

  • 只对独立变量使用原子操作。
  • 对复杂数据结构(如链表、队列)仍需使用互斥量或锁-free 设计。

4. 条件变量

当线程需要等待某个状态变化时,可使用 std::condition_variable

#include <condition_variable>
#include <mutex>
#include <queue>

std::mutex q_mutex;
std::condition_variable q_cond;
std::queue <int> q;

void producer() {
    for (int i = 0; i < 10; ++i) {
        {
            std::lock_guard<std::mutex> lock(q_mutex);
            q.push(i);
            std::cout << "Produced: " << i << '\n';
        }
        q_cond.notify_one(); // 唤醒消费者
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(q_mutex);
        q_cond.wait(lock, []{ return !q.empty(); }); // 等待队列非空
        int val = q.front();
        q.pop();
        lock.unlock(); // 释放锁后再处理
        std::cout << "Consumed: " << val << '\n';
        if (val == 9) break; // 结束条件
    }
}

最佳实践

  • wait 的谓词函数(lambda)确保重入安全。
  • 在唤醒后立即解锁再进行耗时操作,减少锁竞争。

5. 读写锁 (std::shared_mutex)

当读操作远多于写操作时,可使用共享锁:

#include <shared_mutex>

std::shared_mutex g_sharedMutex;
std::unordered_map<int, std::string> g_cache;

void writer(int key, const std::string &value) {
    std::unique_lock<std::shared_mutex> lock(g_sharedMutex); // 写者独占
    g_cache[key] = value;
}

std::string reader(int key) {
    std::shared_lock<std::shared_mutex> lock(g_sharedMutex); // 读者共享
    auto it = g_cache.find(key);
    return it != g_cache.end() ? it->second : "";
}

注意

  • 读者锁不互斥,写者锁会阻塞所有读者和写者。
  • 读写锁在极端写多场景下并不合适。

6. 最佳实践小结

场景 推荐同步工具 说明
简单计数器、布尔标记 std::atomic 无锁,性能最高
访问共享容器(如 std::vector, std::map std::mutex + std::lock_guard 简单易用
生产者-消费者 std::condition_variable 等待/通知
大量读少量写 std::shared_mutex 读者共享
需要手动解锁 std::unique_lock 可自定义锁释放时间

另外,始终遵循 最小化锁粒度锁的持有时间尽量短避免死锁(如统一锁顺序、使用 try_lock) 的原则,才能写出既安全又高效的多线程 C++ 代码。


7. 结语

C++ 标准库为多线程同步提供了丰富而强大的工具。通过合理组合 mutex, atomic, condition_variable, shared_mutex 等原语,并遵循上述最佳实践,开发者可以在保持代码可读性的同时,避免常见的并发错误,从而构建高性能、可维护的并发程序。祝编码愉快!

发表评论