初识并发

Posted by danielh on September 23, 2025

好的,C++ 实现并发主要依赖于标准库中的 <thread> 库(C++11 引入)。我将从简单到复杂,用生动的例子带你理解如何实现。

核心概念:std::thread

std::thread 是 C++ 中表示单个执行线程的类。创建一个 thread 对象就相当于告诉操作系统:“去开辟一个新线程,执行我指定的任务!”


示例 1:最简单的并发 - 创建线程

想象一下,你一边在听音乐(任务A),一边在写文档(任务B)。这两个任务就是并发执行的。

#include <iostream>
#include <thread> // 包含线程库
#include <chrono> // 用于时间操作

// 任务A:模拟“听音乐”
void listenToMusic() {
    for (int i = 0; i < 5; ++i) {
        std::cout << "♪ Listening to music... (" << i+1 << "/5)\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 暂停500毫秒,模拟耗时
    }
}

// 任务B:模拟“写文档”
void writeDocument() {
    for (int i = 0; i < 5; ++i) {
        std::cout << "📝 Writing document... (" << i+1 << "/5)\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(300)); // 暂停300毫秒
    }
}

int main() {
    std::cout << "🚀 Main thread starts. Creating worker threads...\n";

    // 创建并启动线程!
    // thread_obj(函数名) 
    std::thread musicThread(listenToMusic); // 开辟新线程执行listenToMusic
    std::cout << "--> Music thread created!\n";
    std::thread docThread(writeDocument);   // 开辟新线程执行writeDocument
    std::cout << "--> Doc thread created!\n";

    // 等待线程结束!
    // 这就像你必须等两件事都做完才能关机。
    // 如果不等待,主线程结束会强制终止所有子线程。
    musicThread.join();
    std::cout << "--> Music thread finished!\n";
    docThread.join();
    std::cout << "--> Doc thread finished!\n";

    std::cout << "✅ All tasks done! Main thread ends.\n";
    return 0;
}

输出可能如下(每次运行顺序可能不同,这正是并发的特点!):

🚀 Main thread starts. Creating worker threads...
--> Music thread created!
--> Doc thread created!
📝 Writing document... (1/5)
♪ Listening to music... (1/5)
📝 Writing document... (2/5)
♪ Listening to music... (2/5)
📝 Writing document... (3/5)
♪ Listening to music... (3/5)
📝 Writing document... (4/5)
📝 Writing document... (5/5)
♪ Listening to music... (4/5)
♪ Listening to music... (5/5)
--> Music thread finished!
--> Doc thread finished!
✅ All tasks done! Main thread ends.

关键点:

  1. std::thread t(function_name): 创建线程并立即开始执行。
  2. t.join(): 主线程阻塞在这里,等待线程 t 执行完毕。必须等待你创建的所有线程,否则主函数退出会导致程序崩溃。
  3. 输出是交错混合的,证明两个函数确实在同时运行

示例 2:并发带来的问题 - 数据竞争

现在,假设你和你的室友共享一个银行账户(共享数据),你们俩同时去取钱(并发操作)。

#include <iostream>
#include <thread>

int shared_account = 1000; // 共享账户,初始有1000元

// 任务:取钱
void withdrawMoney(int amount) {
    if (shared_account >= amount) {
        // 模拟一个耗时的操作,比如检查信用、连接数据库等
        // 这给了另一个线程可乘之机!
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        shared_account -= amount;
        std::cout << amount << " withdrawn. Remaining: " << shared_account << "\n";
    } else {
        std::cout << "Failed to withdraw " << amount << ". Not enough money.\n";
    }
}

int main() {
    std::cout << "Initial balance: " << shared_account << "\n";

    // 你和室友几乎同时在不同ATM上操作
    std::thread you(withdrawMoney, 800);
    std::thread roommate(withdrawMoney, 800);

    you.join();
    roommate.join();

    std::cout << "Final balance: " << shared_account << "\n";
    return 0;
}

一个可能的错误输出:

Initial balance: 1000
800 withdrawn. Remaining: 200
800 withdrawn. Remaining: -600 // 糟糕!余额成负数了!
Final balance: -600

问题所在: 两个线程同时检查 shared_account (1000) >= 800,条件都成立。然后都去执行 sleep,最后都执行了 shared_account -= 800。结果就是账户被扣了两次 800。

这就是著名的数据竞争。并发编程的核心之一就是解决这种问题。


示例 3:解决并发问题 - 使用互斥锁 (Mutex)

为了解决上面的问题,我们需要让“检查账户余额和扣款”这个操作变成一个原子操作(不可分割的整体)。我们给ATM机加一把锁(Mutex)。

#include <iostream>
#include <thread>
#include <mutex> // 包含互斥锁库

int shared_account = 1000;
std::mutex atm_mutex; // 创建一个互斥锁,就像ATM机的那把锁

void safeWithdrawMoney(int amount) {
    // 在进入临界区(操作共享数据)前上锁
    atm_mutex.lock();

    // 从这里开始,其他线程如果也想lock,就会被阻塞,直到我unlock
    if (shared_account >= amount) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        shared_account -= amount;
        std::cout << amount << " withdrawn. Remaining: " << shared_account << "\n";
    } else {
        std::cout << "Failed to withdraw " << amount << ". Not enough money.\n";
    }

    // 操作完成,释放锁,让其他线程可以进来
    atm_mutex.unlock();
}

int main() {
    std::cout << "Initial balance: " << shared_account << "\n";

    std::thread you(safeWithdrawMoney, 800);
    std::thread roommate(safeWithdrawMoney, 800);

    you.join();
    roommate.join();

    std::cout << "Final balance: " << shared_account << "\n";
    return 0;
}

现在输出总是正确的:

Initial balance: 1000
800 withdrawn. Remaining: 200
Failed to withdraw 800. Not enough money.
Final balance: 200

或者

Initial balance: 1000
800 withdrawn. Remaining: 200
Failed to withdraw 800. Not enough money.
Final balance: 200

更推荐的写法:使用 std::lock_guard 手动 lockunlock 很麻烦,且容易忘记 unlock。std::lock_guard 是一个RAII类,在构造时自动加锁,在析构时(如函数返回、抛出异常)自动解锁,非常安全。

void verySafeWithdrawMoney(int amount) {
    // 创建lock_guard对象时,它自动调用atm_mutex.lock()
    std::lock_guard<std::mutex> guard(atm_mutex);

    // 临界区
    if (shared_account >= amount) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        shared_account -= amount;
        std::cout << amount << " withdrawn. Remaining: " << shared_account << "\n";
    } else {
        // 函数结束时,guard析构,自动调用atm_mutex.unlock()
        std::cout << "Failed to withdraw " << amount << ". Not enough money.\n";
    }
    // 函数结束,guard析构,自动解锁
}

总结:C++ 实现并发的基本步骤

  1. 包含头文件#include <thread>, #include <mutex>
  2. 创建线程对象std::thread t(func, arg1, arg2, ...)
  3. 管理线程生命周期
    • t.join(): 等待线程结束。
    • t.detach(): 分离线程,让其自行运行(使用需非常谨慎)。
  4. 保护共享数据:使用 std::mutexstd::lock_guard(或其他锁)来避免数据竞争。
  5. 同步操作:除了互斥锁,C++ 还提供了 <condition_variable><future> 等工具来进行更复杂的线程间同步和通信。

这就是 C++ 实现并发编程的基础。它给了你强大的底层控制能力,但也要求程序员仔细地处理同步问题。