一、多线程就像厨房里的厨师

想象一下,你开了一家餐厅,后厨有多个厨师同时做菜。如果两个厨师同时抢同一把菜刀,或者互相等着对方先放下调料瓶,这顿饭就做不成了——这就是典型的数据竞争和死锁。在C++里,线程就像这些厨师,而我们要做的就是制定好"厨房管理规则"。

先看个简单例子(技术栈:C++11标准线程库):

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

std::mutex knife;  // 唯一的菜刀
int soup = 0;      // 共享的汤锅

void chef_work() {
    for (int i=0; i<10000; ++i) {
        knife.lock();    // 拿到菜刀才能切菜
        soup++;          // 往汤锅里加料
        knife.unlock();  // 放下菜刀
    }
}

int main() {
    std::thread chefs[3];
    for (auto &c : chefs) {
        c = std::thread(chef_work);
    }
    for (auto &c : chefs) {
        c.join();
    }
    std::cout << "最终汤的浓度: " << soup;  // 正确输出30000
}

这个例子中,std::mutex就像菜刀的借用登记表,保证同一时间只有一个厨师能使用关键资源。注意三点:

  1. 锁的范围要足够小(只包裹soup++
  2. 必须记得unlock(可以用后面介绍的lock_guard自动解决)
  3. 所有线程要用相同的加锁顺序

二、死锁:当厨师们陷入僵局

如果厨师A拿着菜刀等勺子,厨师B拿着勺子等菜刀,这就是死锁。在代码中表现为:

std::mutex knife, spoon;

void chef_A() {
    knife.lock();
    std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟切菜耗时
    spoon.lock();  // 这里会永远等待
    // ...做饭操作
    spoon.unlock();
    knife.unlock();
}

void chef_B() {
    spoon.lock();
    knife.lock();  // 这里会永远等待
    // ...做饭操作
    knife.unlock();
    spoon.unlock();
}

解决方法有四种:

  1. 固定加锁顺序:都先拿菜刀再拿勺子
  2. 使用std::lock:原子性地同时获取多个锁
  3. 超时机制:用try_lock_for设置等待时限
  4. 层级锁:给锁分配优先级

推荐第一种方案:

void safe_chef() {
    std::lock(knife, spoon);  // 同时获取
    std::lock_guard<std::mutex> lk1(knife, std::adopt_lock);
    std::lock_guard<std::mutex> lk2(spoon, std::adopt_lock);
    // 自动解锁机制
}

三、高级武器库:C++提供的工具

除了基础的mutex,现代C++还提供了更趁手的工具:

3.1 自动门卫:lock_guard

void auto_lock_example() {
    std::mutex mtx;
    {
        std::lock_guard<std::mutex> guard(mtx); // 进门自动上锁
        // 临界区操作
    } // 离开作用域自动解锁
}

3.2 灵活锁:unique_lock

支持延迟上锁和所有权转移:

void transfer_lock() {
    std::mutex mtx;
    std::unique_lock<std::mutex> ulock(mtx, std::defer_lock);
    ulock.lock();  // 手动上锁时机
    
    std::thread t([ulock = std::move(ulock)] {
        // 所有权转移到新线程
    });
}

3.3 读写分离:shared_mutex

适合读多写少的场景:

std::shared_mutex file_mutex;

void read_file() {
    std::shared_lock lock(file_mutex); // 多个读者可同时进入
    // 读取操作
}

void write_file() {
    std::unique_lock lock(file_mutex); // 写者独占
    // 写入操作
}

四、实战中的生存法则

4.1 警惕隐藏陷阱

class Dangerous {
    std::mutex mtx;
    std::string data;
public:
    void bad_idea() {
        mtx.lock();
        data = get_network_data(); // 可能抛异常导致锁未释放!
        mtx.unlock();
    }
};

正确做法:始终使用RAII对象管理锁

4.2 性能优化技巧

  • 使用std::atomic替代简单变量的锁
  • 减小临界区范围(把非关键操作移出锁)
  • 考虑无锁数据结构(如boost::lockfree)

4.3 调试建议

  1. 使用thread sanitizer工具检测数据竞争
  2. 给mutex命名便于调试
  3. 记录锁的获取顺序

五、总结:多线程编程的黄金法则

  1. 最小化共享:能不共享的变量尽量线程私有
  2. 精确加锁:锁的范围要尽可能小
  3. 顺序一致:所有线程按相同顺序获取锁
  4. 工具优先:多用标准库提供的RAII锁
  5. 测试从严:线程问题可能在高压下才暴露

记住:好的多线程代码应该像运转良好的厨房——每个厨师都知道自己的工具在哪,工作时互不干扰,最终共同做出一道完美的大餐。