多线程编程:std::thread 与 std::async,线程安全队列,锁与条件变量,死锁预防

多线程编程,说白了就是让程序同时干好几件事。我刚开始接触多线程时,觉得这玩意儿挺酷的——能并行执行,性能肯定翻倍。结果第一次上线就出了大问题,数据乱成一锅粥。嗯,从那以后我才明白,多线程不是简单的「开几个线程就完事」,它需要一套严谨的工程方法论。

今天我们就来聊聊 C++ 多线程编程的核心内容。我会结合自己的实战经验,把 std::thread、std::async、线程安全队列、锁与条件变量、死锁预防这些知识点串起来。

多线程编程核心知识体系 多线程编程 线程创建与执行 同步与互斥 线程安全容器 std::thread std::async std::mutex 条件变量 线程安全队列 死锁预防 核心原则:避免数据竞争 → 保证线程安全 → 防止死锁

一、std::thread:最基础的线程工具

std::thread 是 C++11 引入的线程类。我个人习惯用它来执行那些「跑完就结束」的任务。比如后台日志写入、数据预处理等。

#include <thread>
#include <iostream>

void worker(int id) {
    std::cout << "线程 " << id << " 开始工作\n";
    // 模拟耗时操作
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "线程 " << id << " 工作结束\n";
}

int main() {
    std::thread t1(worker, 1);
    std::thread t2(worker, 2);
    
    t1.join();  // 等待线程结束
    t2.join();
    
    return 0;
}
注意:std::thread 对象销毁前必须调用 join() 或 detach(),否则程序会直接终止。我曾经在项目里忘记 join,结果调试了半天才发现是线程析构时抛了异常。

二、std::async:更优雅的异步任务

std::async 比 std::thread 更高级。它会返回一个 std::future 对象,你可以通过它获取线程的执行结果。说白了,std::async 就是「把任务丢出去,回头再拿结果」。

#include <future>
#include <iostream>

int compute(int x) {
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return x * x;
}

int main() {
    std::future<int> result = std::async(std::launch::async, compute, 10);
    
    // 干点别的事...
    std::cout << "等待计算结果...\n";
    
    // 获取结果(会阻塞直到计算完成)
    int value = result.get();
    std::cout << "结果是: " << value << "\n";
    
    return 0;
}
小技巧:std::async 有两种启动策略:std::launch::async(立即在新线程执行)和 std::launch::deferred(延迟到 get() 时执行)。我建议显式指定策略,避免依赖默认行为。

三、线程安全队列:生产-消费模式的核心

多线程编程中,队列是最常用的数据结构之一。但标准库的 std::queue 不是线程安全的。你需要自己封装一个线程安全版本。

我在项目中遇到过这样的场景:一个线程负责接收网络数据,多个线程负责处理数据。这时候就需要一个线程安全的队列来做缓冲。

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

template<typename T>
class ThreadSafeQueue {
private:
    std::queue<T> queue_;
    mutable std::mutex mutex_;
    std::condition_variable cond_;
    
public:
    void push(T value) {
        std::lock_guard<std::mutex> lock(mutex_);
        queue_.push(std::move(value));
        cond_.notify_one();  // 通知等待的消费者
    }
    
    T pop() {
        std::unique_lock<std::mutex> lock(mutex_);
        cond_.wait(lock, [this] { return !queue_.empty(); });
        T value = std::move(queue_.front());
        queue_.pop();
        return value;
    }
    
    bool try_pop(T& value) {
        std::lock_guard<std::mutex> lock(mutex_);
        if (queue_.empty()) return false;
        value = std::move(queue_.front());
        queue_.pop();
        return true;
    }
};
关键点:条件变量 cond_.wait() 会释放锁并阻塞,直到被 notify 且条件满足。这避免了忙等待(busy waiting),节省 CPU 资源。

四、锁与条件变量:线程间的「交通指挥」

锁(std::mutex)用来保护共享数据,条件变量(std::condition_variable)用来协调线程的执行顺序。两者经常配合使用。

举个例子:一个线程生产数据,另一个线程消费数据。消费者需要等生产者生产完才能消费。这时候条件变量就派上用场了。

#include <mutex>
#include <condition_variable>

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

void producer() {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    {
        std::lock_guard<std::mutex> lock(mtx);
        ready = true;
        std::cout << "生产者: 数据准备好了\n";
    }
    cv.notify_one();  // 通知消费者
}

void consumer() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, [] { return ready; });  // 等待条件满足
    std::cout << "消费者: 收到数据,开始处理\n";
}
注意:条件变量的 wait() 必须配合 unique_lock 使用,不能使用 lock_guard。因为 wait() 需要临时释放锁,lock_guard 不支持这个操作。

五、死锁预防:别让线程「互相等待」

死锁是多线程编程中最头疼的问题之一。说白了就是两个线程互相持有对方需要的锁,谁也不肯放手,结果大家都卡住了。

我曾经在项目中遇到过死锁:线程 A 持有锁 1 等待锁 2,线程 B 持有锁 2 等待锁 1。结果整个系统卡死了,线上报警响个不停。从那以后,我总结了几条死锁预防的黄金法则:

预防策略 说明 推荐度
固定加锁顺序 所有线程按相同顺序获取锁 ⭐⭐⭐⭐⭐
使用 std::lock() 一次性锁定多个互斥量,避免死锁 ⭐⭐⭐⭐
避免嵌套锁 尽量只持有一个锁 ⭐⭐⭐⭐
使用超时锁 try_lock_for() 或 try_lock_until() ⭐⭐⭐
// 使用 std::lock() 避免死锁
std::mutex mtx1, mtx2;

void safe_operation() {
    // 一次性锁定两个互斥量
    std::lock(mtx1, mtx2);
    
    // 使用 adopt_lock 表示锁已经被锁定
    std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
    
    // 安全地操作共享数据
    // ...
}
避坑指南:我曾经在代码里用了嵌套锁,结果调试了整整一天才发现是死锁。后来我养成了一个习惯:每次加锁都问自己「这个锁的顺序是否和别的线程一致?」。如果答案是否定的,那就赶紧改。

六、实战经验总结

多线程编程没有银弹。你想想看,每个项目的情况都不一样。但有几条原则是通用的:

  • 尽量使用 std::async:它比 std::thread 更安全,自动管理线程生命周期。
  • 锁的粒度要小:只保护必要的代码段,不要锁住整个函数。
  • 优先使用 std::lock_guard:RAII 风格,异常安全。
  • 条件变量一定要配合 while 循环:防止虚假唤醒(spurious wakeup)。
  • 死锁预防要提前设计:不要等到出问题了再补救。

我个人习惯在项目初期就画好线程交互图,明确每个线程的职责和锁的获取顺序。这样能避免很多后期的问题。

核心思想:多线程编程的本质是「共享资源的访问控制」。只要把「谁在什么时候可以访问什么数据」理清楚,多线程就没那么可怕。

公众号:蓝海资料掘金营,微信deep3321