好的,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.
关键点:
std::thread t(function_name)
: 创建线程并立即开始执行。t.join()
: 主线程阻塞在这里,等待线程t
执行完毕。必须等待你创建的所有线程,否则主函数退出会导致程序崩溃。- 输出是交错混合的,证明两个函数确实在同时运行。
示例 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
手动 lock
和 unlock
很麻烦,且容易忘记 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++ 实现并发的基本步骤
- 包含头文件:
#include <thread>
,#include <mutex>
- 创建线程对象:
std::thread t(func, arg1, arg2, ...)
- 管理线程生命周期:
t.join()
: 等待线程结束。t.detach()
: 分离线程,让其自行运行(使用需非常谨慎)。
- 保护共享数据:使用
std::mutex
和std::lock_guard
(或其他锁)来避免数据竞争。 - 同步操作:除了互斥锁,C++ 还提供了
<condition_variable>
和<future>
等工具来进行更复杂的线程间同步和通信。
这就是 C++ 实现并发编程的基础。它给了你强大的底层控制能力,但也要求程序员仔细地处理同步问题。