文章

C++ 并发编程入门

C++ 并发编程入门

并发编程是指在多线程或多进程环境下,协调多个执行流对共享资源的访问,以避免数据竞争(data race)和状态不一致。在 C++ 中,我们通常使用互斥锁(std::mutex)来保护临界区,确保同一时刻只有一个线程能访问共享资源。


std::mutex 与 RAII 锁管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <thread>
#include <mutex>

int number = 0;
std::mutex _mutex;

void number_adder(const int& value) {
    // 推荐:使用 lock_guard 自动管理锁(RAII)
    std::lock_guard lock(_mutex);
    number += value;
    // lock_guard 析构时自动解锁,避免死锁
}

int main() {
    int value = 10;
    std::thread t1(number_adder, std::ref(value));
    std::thread t2(number_adder, std::ref(value));

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

    std::cout << "number = " << number << std::endl; // 输出: 20
    return 0;
}

在这个例子中,两个线程同时对全局变量 number 执行加法操作。若无锁保护,可能出现以下竞态条件:

  1. 线程 t1 读取 number = 0
  2. 线程 t2 也读取 number = 0
  3. 两者分别计算 0 + 10 = 10
  4. 先后写回,最终结果为 10 而非预期的 20

通过互斥锁保护,确保加法操作的原子性,从而得到正确结果。


std::condition_variable:条件等待与协作

std::mutex 解决了“互斥访问”,但无法处理“条件不满足时需等待”的场景。例如:消费者线程希望从队列中取数据,但队列为空——此时不应忙等待,而应释放锁并阻塞,直到生产者放入新数据。

为此,C++ 提供了 std::condition_variable,配合 std::unique_lock 实现高效等待。

线程安全队列示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <queue>
#include <mutex>
#include <condition_variable>

template
class ThreadSafeQueue {
private:
    std::queue _m_queue;
    mutable std::mutex _m_mutex;
    std::condition_variable _m_cond;

public:
    ThreadSafeQueue() = default;
    ThreadSafeQueue(const ThreadSafeQueue&) = delete;
    ThreadSafeQueue& operator=(const ThreadSafeQueue&) = delete;

    void push(const T& val) {
        {
            std::lock_guard lock(_m_mutex);
            _m_queue.push(val);
        } // 自动解锁
        _m_cond.notify_one(); // 唤醒一个等待的消费者
    }

    void pop(T& val) {
        std::unique_lock lock(_m_mutex);
        // wait 会:1) 检查谓词;2) 若为 false,则原子地释放锁并阻塞
        //          3) 被唤醒后重新获取锁,再次检查谓词
        _m_cond.wait(lock, [this] { return !_m_queue.empty(); });

        val = std::move(_m_queue.front());
        _m_queue.pop();
    }
};

关键点解析

  • 为什么 pop 必须用 unique_lock 而不是 lock_guard
    因为 condition_variable::wait() 需要在等待时临时释放锁,并在被唤醒后自动重新加锁lock_guard 不支持手动解锁,而 unique_lock 支持,因此是唯一合法选择。

  • wait 的谓词(predicate)为何必要?
    防止“虚假唤醒”(spurious wakeup)——即使没有调用 notify,系统也可能唤醒等待线程。通过循环检查条件(!_m_queue.empty()),确保只有条件真正满足时才继续执行。

  • notify_one() vs notify_all()
    此处只需唤醒一个消费者,故用 notify_one() 更高效。若多个线程等待不同条件,则需 notify_all()


总结

  • std::mutex:提供互斥访问,防止数据竞争。
  • std::lock_guard:基于 RAII 的自动锁管理,适用于简单临界区。
  • std::unique_lock + std::condition_variable:用于“条件等待”场景,支持在等待时释放锁,避免死锁和资源浪费。

现代多核设备普遍支持并发执行。对于 I/O 密集型任务(如网络请求、文件读写),多线程可将计算I/O 等待解耦:主线程提交 I/O 任务后立即处理下一工作,无需阻塞等待,从而显著提升系统吞吐量与响应性。

并发编程虽强大,但也引入了复杂性。始终遵循:最小化临界区、避免嵌套锁、优先使用 RAII、善用条件变量,才能写出高效且安全的并发代码。

本文由作者按照 CC BY 4.0 进行授权