### C++ 并发编程中的原子操作与互斥锁:如何选择最佳方案?

在多线程 C++ 程序中,线程安全是最关键的设计要点。常见的同步机制主要有两类:原子操作std::atomic)和互斥锁std::mutex)。虽然两者都能保证数据的一致性,但在性能、可读性和使用场景上存在显著差异。本文将结合实际代码示例,分析何时使用 std::atomic,何时使用 std::mutex,以及两者如何协同工作。


1. 原子操作(std::atomic

1.1 基本概念

std::atomic 是一种无锁编程工具,提供了对原子类型的线程安全访问。它通过硬件指令实现原子性,避免了传统互斥锁所产生的上下文切换和堆栈拷贝开销。

1.2 常见用例

  • 计数器:线程安全地递增/递减
  • 标志位:例如停止信号、完成标志
  • 交换值std::atomic::exchange() 用于快速更新
#include <atomic>
#include <thread>
#include <vector>
#include <iostream>

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

void worker() {
    for (int i = 0; i < 10000; ++i) {
        counter.fetch_add(1, std::memory_order_relaxed);
    }
}

int main() {
    std::vector<std::thread> workers(8);
    for (auto& t : workers) t = std::thread(worker);
    for (auto& t : workers) t.join();
    std::cout << "Final counter: " << counter.load() << std::endl;
}

1.3 内存序(Memory Order)

  • std::memory_order_relaxed:最快,但不保证可见性
  • std::memory_order_acquire / std::memory_order_release:常用于生产者-消费者模式
  • std::memory_order_seq_cst:默认强序,最安全但成本最高

2. 互斥锁(std::mutex

2.1 基本概念

std::mutex 提供了基于锁的同步机制,确保同一时刻只有一个线程访问受保护的资源。它的实现通常依赖操作系统原语(如 pthread_mutex),并可能涉及上下文切换。

2.2 常见用例

  • 复杂数据结构:如 std::vectorstd::map 的读写
  • 共享资源:文件句柄、网络连接
  • 需要批量操作:一次性锁定多变量
#include <mutex>
#include <vector>
#include <thread>
#include <iostream>

std::vector <int> sharedVec;
std::mutex vecMutex;

void addToVector(int val) {
    std::lock_guard<std::mutex> lock(vecMutex);
    sharedVec.push_back(val);
}

int main() {
    std::vector<std::thread> t(4);
    for (int i = 0; i < 4; ++i) {
        t[i] = std::thread([i]{
            for (int j = 0; j < 100; ++j) addToVector(i * 100 + j);
        });
    }
    for (auto& th : t) th.join();
    std::cout << "Vector size: " << sharedVec.size() << std::endl;
}

2.3 std::unique_lockstd::scoped_lock

  • std::unique_lock 可在同一作用域内多次锁定/解锁
  • std::scoped_lock 可一次性锁定多个互斥量,防止死锁
std::mutex m1, m2;
{
    std::scoped_lock lock(m1, m2); // 同时锁定 m1 和 m2
    // ...
}

3. 原子 vs 互斥:何时使用哪种

场景 推荐使用
需要极致性能,且操作仅为简单读/写/增/减 std::atomic
操作复杂,需要多变量协同变更 std::mutex
只需读写一个标志或计数器,且多线程并发量大 std::atomic
需要对数据结构做批量修改,或有大量锁竞争 std::mutex
需要保证可见性或顺序(例如生产者-消费者) 结合 memory_order_acquire/releasestd::atomicstd::mutex

关键:如果某个变量只需要单个原子操作(读、写、交换),直接使用 std::atomic 更简洁且性能更佳;若需要在多线程间同步复杂状态或多个相关变量,互斥锁是更安全的选择。


4. 混合使用:原子 + 互斥的最佳实践

在实际项目中,往往需要将两者结合使用。例如,使用 std::atomic 控制一个 活动线程数,并使用 std::mutex 保护一个共享队列。

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

std::queue <int> taskQueue;
std::mutex queueMutex;
std::atomic <int> activeThreads{0};

void worker() {
    while (true) {
        int task;
        {
            std::lock_guard<std::mutex> lock(queueMutex);
            if (taskQueue.empty()) break;
            task = taskQueue.front();
            taskQueue.pop();
        }
        // 处理任务
        std::cout << "Thread " << std::this_thread::get_id() << " processing " << task << std::endl;
    }
    activeThreads.fetch_sub(1, std::memory_order_relaxed);
}

int main() {
    // 初始化任务
    for (int i = 0; i < 20; ++i) taskQueue.push(i);
    const int numThreads = 4;
    activeThreads.store(numThreads, std::memory_order_relaxed);
    std::vector<std::thread> workers(numThreads);
    for (int i = 0; i < numThreads; ++i)
        workers[i] = std::thread(worker);
    for (auto& th : workers) th.join();
    std::cout << "All tasks completed. Active threads: " << activeThreads.load() << std::endl;
}
  • activeThreads 通过 std::atomic 记录工作线程数量,避免频繁锁定。
  • 共享队列 taskQueuestd::mutex 保护,保证一次只被一个线程访问。

5. 小结

  • std::atomic:无锁、低开销,适合简单原子操作(计数器、标志位)。
  • std::mutex:传统锁,适合复杂数据结构或需要批量操作的场景。
  • 内存序:合理选择 memory_order 能显著提升性能。
  • 混合使用:在高并发系统中,两者常结合使用,既保持性能,又保证正确性。

掌握这些同步工具后,你可以根据具体需求,灵活选择最合适的方案,构建既高效又可靠的多线程 C++ 程序。

发表评论