✅ 本篇目标:
- 理解 线程(
std::thread) 的基本使用- 掌握 互斥锁(
std::mutex) 解决数据竞争- 学会使用
std::async和std::future简化异步任务- 实战项目:并行计算素数个数 + 线程安全日志系统
- 初识 死锁 与 避免策略
🕒 建议学习时间:4--5 小时|可分多次完成💡 本篇将让你写出 高性能、响应迅速、充分利用多核 CPU 的 C++ 程序!
📘 C++ 零基础入门教程(第 8 篇)
多线程与并发编程 ------ 让程序"同时做多件事"
第一步:为什么需要多线程?
单线程的局限:
cpp
void downloadFile() { /* 耗时 5 秒 */ }
void processData() { /* 耗时 3 秒 */ }
int main() {
downloadFile(); // 等 5 秒
processData(); // 再等 3 秒 → 总耗时 8 秒
}
✅ 多线程:并行执行
- 同时下载和处理(如果逻辑允许)
- 充分利用多核 CPU
- 提升用户体验(UI 不卡顿)
⚠️ 注意:不是所有任务都能并行!需考虑 数据依赖 和 共享状态。
第二步:std::thread ------ 创建线程
基本用法:
cpp
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
void task1() {
this_thread::sleep_for(2s); // 模拟耗时操作
cout << "任务1完成\n";
}
void task2(int id) {
cout << "任务2(ID=" << id << ")开始\n";
this_thread::sleep_for(1s);
cout << "任务2完成\n";
}
int main() {
thread t1(task1); // 启动线程 t1
thread t2(task2, 42); // 传参启动 t2
cout << "主线程继续运行...\n";
t1.join(); // 等待 t1 结束
t2.join(); // 等待 t2 结束
cout << "所有任务完成!\n";
return 0;
}
✅ 可能输出(顺序不确定):
主线程继续运行...
任务2(ID=42)开始
任务2完成
任务1完成
所有任务完成!
🔑 关键点:
thread t(func, args...)启动新线程- 必须调用
join()或detach(),否则程序终止时崩溃join():阻塞当前线程,直到目标线程结束detach():让线程在后台运行(慎用,生命周期难控)
第三步:数据竞争(Data Race)与互斥锁
❌ 危险示例:多个线程修改同一变量
cpp
int counter = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
++counter; // 非原子操作!可能出错
}
}
int main() {
thread t1(increment);
thread t2(increment);
t1.join();
t2.join();
cout << "最终 counter = " << counter << endl; // 期望 200000,但常小于该值!
}
🔥 数据竞争 :多个线程同时读写同一内存,且无同步 → 未定义行为!
✅ 解决方案:std::mutex(互斥锁)
cpp
#include <mutex>
int counter = 0;
mutex mtx; // 互斥锁
void safeIncrement() {
for (int i = 0; i < 100000; ++i) {
lock_guard<mutex> lock(mtx); // 自动加锁/解锁(RAII!)
++counter;
}
}
💡
lock_guard是 RAII 封装:
- 构造时自动
lock()- 析构时自动
unlock()- 即使抛异常也能保证解锁!
✅ 现在输出总是:
最终 counter = 200000
第四步:std::async 与 std::future ------ 更简单的异步
如果你只是想"启动一个任务,稍后拿结果",async 比 thread 更方便。
示例:异步计算平方
cpp
#include <future>
#include <iostream>
using namespace std;
int square(int x) {
this_thread::sleep_for(1s);
return x * x;
}
int main() {
// 启动异步任务
future<int> result = async(square, 10);
cout << "主线程做其他事...\n";
// 获取结果(会阻塞直到完成)
int value = result.get();
cout << "10 的平方是 " << value << endl;
return 0;
}
✅ 输出:
主线程做其他事...
10 的平方是 100
🎯 优势:
- 自动管理线程
- 通过
future安全传递结果- 支持异常传递(见下文)
异常也能跨线程传递!
cpp
int mightThrow(int x) {
if (x < 0) throw runtime_error("负数无效!");
return x * x;
}
int main() {
auto f = async(mightThrow, -5);
try {
cout << f.get(); // 抛出子线程中的异常!
} catch (const exception& e) {
cout << "捕获异常: " << e.what() << endl;
}
}
✅ 输出:
捕获异常: 负数无效!
第五步:实战项目 1 ------ 并行计算 [1, N] 中的素数个数
我们将把大任务拆成小块,并行计算。
cpp
// parallel_prime.cpp
#include <iostream>
#include <vector>
#include <thread>
#include <mutex>
#include <cmath>
using namespace std;
bool isPrime(int n) {
if (n < 2) return false;
if (n == 2) return true;
if (n % 2 == 0) return false;
for (int i = 3; i <= sqrt(n); i += 2) {
if (n % i == 0) return false;
}
return true;
}
// 全局结果(需保护)
int totalPrimes = 0;
mutex primeMutex;
void countPrimesInRange(int start, int end) {
int localCount = 0;
for (int i = start; i <= end; ++i) {
if (isPrime(i)) {
++localCount;
}
}
// 仅在最后更新全局变量(减少锁开销)
lock_guard<mutex> lock(primeMutex);
totalPrimes += localCount;
}
int main() {
const int N = 100000;
const int numThreads = 4;
const int chunkSize = N / numThreads;
vector<thread> threads;
for (int i = 0; i < numThreads; ++i) {
int start = i * chunkSize + 1;
int end = (i == numThreads - 1) ? N : (i + 1) * chunkSize;
threads.emplace_back(countPrimesInRange, start, end);
}
for (auto& t : threads) {
t.join();
}
cout << "1 到 " << N << " 中有 " << totalPrimes << " 个素数\n";
return 0;
}
💡 优化技巧:
- 每个线程先计算局部计数,最后一次性加到全局 → 减少锁竞争
- 对比单线程版本,速度提升接近 4 倍(在 4 核 CPU 上)
第六步:实战项目 2 ------ 线程安全日志系统
多线程程序需要安全地写日志。
cpp
// thread_safe_logger.h
#pragma once
#include <iostream>
#include <mutex>
#include <string>
class Logger {
private:
static std::mutex logMutex;
public:
template<typename... Args>
static void log(Args&&... args) {
std::lock_guard<std::mutex> lock(logMutex);
((std::cout << args << " "), ...); // C++17 折叠表达式
std::cout << std::endl;
}
};
// 在 .cpp 中定义静态成员
std::mutex Logger::logMutex;
使用:
cpp
void worker(int id) {
for (int i = 0; i < 3; ++i) {
Logger::log("线程", id, "正在工作,步骤", i);
this_thread::sleep_for(500ms);
}
}
int main() {
std::thread t1(worker, 1);
std::thread t2(worker, 2);
t1.join(); t2.join();
return 0;
}
✅ 输出(无交错乱码):
线程 1 正在工作,步骤 0
线程 2 正在工作,步骤 0
线程 1 正在工作,步骤 1
线程 2 正在工作,步骤 1
...
第七步:死锁(Deadlock)与如何避免
❌ 死锁示例:
cpp
mutex m1, m2;
void thread1() {
lock_guard<mutex> lock1(m1);
this_thread::sleep_for(1ms);
lock_guard<mutex> lock2(m2); // 等 m2
}
void thread2() {
lock_guard<mutex> lock2(m2);
this_thread::sleep_for(1ms);
lock_guard<mutex> lock1(m1); // 等 m1 → 死锁!
}
🔥 死锁条件:
- 互斥
- 持有并等待
- 不可抢占
- 循环等待
✅ 避免策略:
-
始终以相同顺序加锁(如先 m1 再 m2)
-
使用
std::lock()一次性锁定多个 mutex:cppstd::lock(m1, m2); lock_guard<mutex> lock1(m1, adopt_lock); lock_guard<mutex> lock2(m2, adopt_lock); -
尽量减少锁的粒度和持有时间
📌 本篇小结:你已掌握
| 概念 | 说明 |
|---|---|
std::thread |
创建和管理线程 |
| 数据竞争 | 多线程同时修改共享数据 → 未定义行为 |
std::mutex + lock_guard |
保证临界区互斥访问 |
std::async / std::future |
简化异步任务与结果获取 |
| 线程安全设计 | 减少共享、最小化锁范围 |
| 死锁 | 成因与避免策略 |
✅ 下一步建议
- 尝试 :用
std::atomic<int>替代mutex实现无锁计数器(适用于简单类型) - 思考 :如何实现"生产者-消费者"模型?(提示:
condition_variable) - 预习:什么是"移动语义进阶"?右值引用与完美转发