C++多线程编程指南
编程小白也能理解的多线程核心知识点
多线程是现代编程中提升程序性能的关键技术。本文用通俗易懂的语言解释C++多线程的核心概念,搭配简单示例,帮助你快速掌握多线程编程!
学习时间: 约30分钟
涵盖6大核心主题
10+个实用代码示例
线程基础概念
什么是线程?
线程是程序执行的最小单位。一个程序可以有多个线程同时运行,就像厨房里多个厨师同时做不同菜肴。
为什么要使用多线程?
- 提高性能: 利用多核CPU同时处理任务
- 响应性: 主线程保持响应,后台线程处理耗时操作
- 效率: 同时执行多个I/O操作(如网络请求)
C++线程基础代码
// 包含线程头文件
#include <thread>
// 线程执行的函数
void threadTask() {
std::cout << “Hello from thread!” << std::endl;
}
int main() {
// 创建线程并执行
std::thread t1(threadTask);
// 等待线程完成
t1.join();
return 0;
}
#include <thread>
// 线程执行的函数
void threadTask() {
std::cout << “Hello from thread!” << std::endl;
}
int main() {
// 创建线程并执行
std::thread t1(threadTask);
// 等待线程完成
t1.join();
return 0;
}
提示: 每个线程都需要独立的栈空间,但共享堆内存和全局变量。
线程同步机制
为什么需要同步?
当多个线程访问共享资源时,可能会发生数据竞争(data race),导致程序行为不可预测。
生活类比: 想象多个收银员同时操作同一个收银机 – 如果没有协调机制,就会发生混乱!
互斥锁 (Mutex)
互斥锁确保同一时间只有一个线程可以访问共享资源。
#include <mutex>
std::mutex mtx; // 创建互斥锁
void safeIncrement(int& counter) {
mtx.lock(); // 上锁
counter++; // 安全操作共享数据
mtx.unlock(); // 解锁
}
std::mutex mtx; // 创建互斥锁
void safeIncrement(int& counter) {
mtx.lock(); // 上锁
counter++; // 安全操作共享数据
mtx.unlock(); // 解锁
}
更安全的方式: lock_guard
使用RAII技术,自动管理锁的生命周期,避免忘记解锁。
void saferIncrement(int& counter) {
std::lock_guard<std::mutex> lock(mtx);
counter++; // 自动上锁和解锁
}
std::lock_guard<std::mutex> lock(mtx);
counter++; // 自动上锁和解锁
}
警告: 避免在持有锁时执行耗时操作,否则会降低多线程性能优势。
条件变量
什么是条件变量?
条件变量允许线程在某些条件不满足时进入等待状态,当条件满足时被唤醒。
生活类比: 就像顾客在餐厅等空桌 – 当有空桌时,服务员通知等待的顾客。
生产者-消费者模式
经典的多线程协作模型:
- 生产者: 生产数据并放入缓冲区
- 消费者: 从缓冲区取出数据并处理
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool dataReady = false;
// 生产者线程
void producer() {
std::lock_guard<std::mutex> lock(mtx);
// 生产数据…
dataReady = true;
cv.notify_one(); // 通知一个消费者
}
// 消费者线程
void consumer() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return dataReady; });
// 消费数据…
}
std::mutex mtx;
std::condition_variable cv;
bool dataReady = false;
// 生产者线程
void producer() {
std::lock_guard<std::mutex> lock(mtx);
// 生产数据…
dataReady = true;
cv.notify_one(); // 通知一个消费者
}
// 消费者线程
void consumer() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return dataReady; });
// 消费数据…
}
提示: 使用条件变量时,总是配合互斥锁和谓词条件,避免虚假唤醒。
原子操作
什么是原子操作?
原子操作是不可分割的操作,不会被线程调度打断,适合简单的计数器场景。
生活类比: 就像自动售货机投币 – 要么投币成功商品掉出,要么失败退回硬币,不会卡在中间状态。
原子操作的优势
- 无需使用锁
- 更高的性能
- 避免死锁风险
原子计数器示例
#include <atomic>
std::atomic<int> counter(0);
void increment() {
for (int i = 0; i < 1000; ++i) {
counter++; // 线程安全的递增
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << “Counter: “ << counter << std::endl; // 正确输出2000
}
std::atomic<int> counter(0);
void increment() {
for (int i = 0; i < 1000; ++i) {
counter++; // 线程安全的递增
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << “Counter: “ << counter << std::endl; // 正确输出2000
}
注意: 原子操作适用于简单数据类型,对于复杂操作仍需使用锁。
Future与Async
异步操作
std::async 和 std::future 提供了更高级的异步操作接口,无需直接管理线程。
生活类比: 就像点外卖 – 下单后继续做其他事情,外卖到了自动通知你。
基本用法
#include <future>
// 一个耗时的计算函数
int compute() {
// 模拟耗时操作
std::this_thread::sleep_for(std::chrono::seconds(2));
return 42; // 返回计算结果
}
int main() {
// 启动异步任务
std::future<int> result = std::async(std::launch::async, compute);
std::cout << “等待结果中…” << std::endl;
// 获取结果(如果还没完成,会阻塞等待)
int value = result.get();
std::cout << “计算结果: “ << value << std::endl;
}
// 一个耗时的计算函数
int compute() {
// 模拟耗时操作
std::this_thread::sleep_for(std::chrono::seconds(2));
return 42; // 返回计算结果
}
int main() {
// 启动异步任务
std::future<int> result = std::async(std::launch::async, compute);
std::cout << “等待结果中…” << std::endl;
// 获取结果(如果还没完成,会阻塞等待)
int value = result.get();
std::cout << “计算结果: “ << value << std::endl;
}
std::async的启动策略
- std::launch::async: 立即在新线程中执行
- std::launch::deferred: 延迟执行,直到调用get()或wait()
最佳实践: 对于简单的异步任务,优先使用std::async而不是直接创建线程。
线程池
为什么需要线程池?
频繁创建和销毁线程开销很大,线程池预先创建一组线程,重复利用它们执行任务。
线程池的优势
- 减少线程创建/销毁开销
- 控制并发线程数量
- 有效管理系统资源
生活类比: 就像客服中心 – 固定数量的客服人员处理不断进来的电话,而不是为每个电话雇佣新客服。
线程池基本结构
#include <vector>
#include <queue>
#include <functional>
#include <thread>
#include <mutex>
#include <condition_variable>
class ThreadPool {
public:
ThreadPool(size_t numThreads) {
for(size_t i = 0; i < numThreads; ++i) {
workers.emplace_back([this] {
while(true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(queueMutex);
condition.wait(lock, [this]{ return stop || !tasks.empty(); });
if(stop && tasks.empty()) return;
task = std::move(tasks.front());
tasks.pop();
}
task();
}
});
}
}
// 省略析构函数和其他方法…
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex queueMutex;
std::condition_variable condition;
bool stop = false;
};
#include <queue>
#include <functional>
#include <thread>
#include <mutex>
#include <condition_variable>
class ThreadPool {
public:
ThreadPool(size_t numThreads) {
for(size_t i = 0; i < numThreads; ++i) {
workers.emplace_back([this] {
while(true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(queueMutex);
condition.wait(lock, [this]{ return stop || !tasks.empty(); });
if(stop && tasks.empty()) return;
task = std::move(tasks.front());
tasks.pop();
}
task();
}
});
}
}
// 省略析构函数和其他方法…
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex queueMutex;
std::condition_variable condition;
bool stop = false;
};
提示: C++11标准库没有提供线程池,但C++20引入了jthread和stop_token,使线程池实现更简单。
多线程编程要点总结
掌握C++多线程编程需要理解以下核心原则:
- 避免数据竞争 – 使用互斥锁或原子操作保护共享数据
- 优先使用高级抽象 – 如std::async和std::future,而不是直接创建线程
- 理解线程生命周期 – 确保线程正确join或detach
- 避免死锁 – 按固定顺序获取锁,或使用std::lock同时锁定多个互斥量
- 使用RAII管理资源 – std::lock_guard, std::unique_lock等
- 考虑性能影响 – 线程创建、上下文切换、锁竞争都有开销
多线程编程就像管理一个团队 – 需要明确分工(任务分解)、良好沟通(线程同步)和协调合作(避免冲突)。
实践建议:从简单例子开始,逐步增加复杂度,使用工具如Valgrind或ThreadSanitizer检测数据竞争和死锁。