多线程编程
多线程是提升程序性能的核心手段(充分利用多核
CPU),但也带来了线程安全、同步互斥等问题。C++11
引入了标准线程库(<thread>),统一了多线程编程接口,替代了平台相关的
pthread(Linux)、CreateThread(Windows)。
1. std::thread
(1)线程创建与启动
std::thread
是线程类,创建对象时指定线程函数,自动启动线程。核心注意事项:
- 线程函数可以是函数指针、函数对象、lambda 表达式;
- 线程启动后必须调用
join()(等待线程结束)或detach()(分离线程,让线程后台运行),否则程序退出时会调用std::terminate()终止; - 传递参数时,默认是按值拷贝,若需传递引用,需用
std::ref()包装。
#include <thread>
#include <string>
#include <iostream>
using namespace std;
// 线程函数1:普通函数
void ThreadFunc1(int a, string msg) {
cout << "ThreadFunc1:a=" << a << ",msg=" << msg << endl;
}
// 线程函数2:函数对象
class ThreadObj {
public:
void operator()(int a) {
cout << "ThreadObj:a=" << a << endl;
}
};
int main() {
// 1. 用普通函数创建线程
thread t1(ThreadFunc1, 10, "hello");
// 2. 用函数对象创建线程
ThreadObj obj;
thread t2(obj, 20);
// 3. 用 lambda 创建线程(最常用)
thread t3([](int a, string msg) {
cout << "lambda 线程:a=" << a << ",msg=" << msg << endl;
}, 30, "world");
// 等待线程结束(必须调用 join() 或 detach())
t1.join();
t2.join();
t3.join();
cout << "主线程结束" << endl;
return 0;
}(2)线程属性
- 线程 ID:用
this_thread::get_id()获取当前线程 ID; - 线程休眠:用
this_thread::sleep_for(duration)让线程休眠(如sleep_for(chrono::seconds(1))休眠 1 秒); - 线程交换:用
swap(t1, t2)或t1.swap(t2)交换两个线程的资源。
#include <thread>
#include <chrono>
#include <iostream>
using namespace std;
void SleepFunc() {
cout << "线程 " << this_thread::get_id() << " 开始休眠" << endl;
this_thread::sleep_for(chrono::milliseconds(500)); // 休眠 500ms
cout << "线程 " << this_thread::get_id() << " 休眠结束" << endl;
}
int main() {
thread t(SleepFunc);
cout << "线程 t 的 ID:" << t.get_id() << endl;
t.join();
return 0;
}2. 线程同步与互斥
多线程访问共享资源时,会出现“竞态条件”(Race
Condition),导致数据错误。线程同步的核心是“保证共享资源的原子操作”,C++
提供了
mutex、lock_guard、unique_lock
等工具。
(1)std::mutex
std::mutex 是最基础的互斥锁,提供
lock()(加锁)和
unlock()(解锁)接口,核心规则:
- 同一时间只有一个线程能成功加锁,其他线程会阻塞直到锁被释放;
- 必须保证
lock()和unlock()成对出现(避免死锁或资源泄漏); - 推荐用
lock_guard或unique_lock自动管理锁(RAII 原则)。
#include <thread>
#include <mutex>
#include <iostream>
using namespace std;
int g_count = 0; // 共享资源
mutex g_mutex; // 互斥锁
// 线程函数:对共享资源进行累加
void Increment() {
for (int i = 0; i < 100000; ++i) {
// 方式1:手动加锁解锁(不推荐,容易遗漏 unlock())
// g_mutex.lock();
// ++g_count;
// g_mutex.unlock();
// 方式2:lock_guard(RAII,自动加锁解锁,推荐)
lock_guard<mutex> lock(g_mutex);
++g_count; // 临界区:原子操作
}
}
int main() {
thread t1(Increment);
thread t2(Increment);
t1.join();
t2.join();
cout << "最终计数:" << g_count << endl; // 正确输出 200000(无竞态条件)
return 0;
}(2)unique_lock
lock_guard
是简单锁管理(构造加锁,析构解锁),unique_lock
更灵活,支持:
- 延迟加锁(
std::defer_lock); - 尝试加锁(
try_lock()); - 超时加锁(
try_lock_for()); - 手动解锁(
unlock())。
#include <thread>
#include <mutex>
#include <chrono>
#include <iostream>
using namespace std;
mutex g_mutex;
void TryLockFunc() {
unique_lock<mutex> lock(g_mutex, defer_lock); // 延迟加锁
if (lock.try_lock_for(chrono::milliseconds(100))) { // 超时 100ms 尝试加锁
cout << "线程 " << this_thread::get_id() << " 加锁成功" << endl;
this_thread::sleep_for(chrono::milliseconds(200)); // 持有锁 200ms
// 析构时自动解锁
} else {
cout << "线程 " << this_thread::get_id() << " 加锁失败(超时)" << endl;
}
}
int main() {
thread t1(TryLockFunc);
thread t2(TryLockFunc);
t1.join();
t2.join();
return 0;
}(3)条件变量(std::condition_variable)
条件变量用于线程间通信,让一个线程等待某个条件满足后再继续执行(如生产者-消费者模型)。核心接口:
wait(lock):释放锁,阻塞线程,直到被notify_one()或notify_all()唤醒;notify_one():唤醒一个等待的线程;notify_all():唤醒所有等待的线程。
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <iostream>
using namespace std;
const int MAX_QUEUE_SIZE = 5;
queue<int> g_queue; // 共享队列(生产者生产,消费者消费)
mutex g_mutex; // 互斥锁
condition_variable g_cv; // 条件变量
// 生产者线程:生产数据放入队列
void Producer() {
for (int i = 1; i <= 10; ++i) {
unique_lock<mutex> lock(g_mutex);
// 等待队列不满(避免队列溢出)
g_cv.wait(lock, []() { return g_queue.size() < MAX_QUEUE_SIZE; });
// 生产数据
g_queue.push(i);
cout << "生产者生产:" << i << ",队列大小:" << g_queue.size() << endl;
lock.unlock();
g_cv.notify_one(); // 唤醒一个消费者
this_thread::sleep_for(chrono::milliseconds(100)); // 模拟生产耗时
}
}
// 消费者线程:从队列获取数据消费
void Consumer() {
while (true) {
unique_lock<mutex> lock(g_mutex);
// 等待队列不为空(避免消费空队列)
g_cv.wait(lock, []() { return !g_queue.empty(); });
// 消费数据
int data = g_queue.front();
g_queue.pop();
cout << "消费者消费:" << data << ",队列大小:" << g_queue.size() << endl;
lock.unlock();
g_cv.notify_one(); // 唤醒一个生产者
if (data == 10) break; // 消费完最后一个数据退出
this_thread::sleep_for(chrono::milliseconds(200)); // 模拟消费耗时
}
}
int main() {
thread producer(Producer);
thread consumer(Consumer);
producer.join();
consumer.join();
return 0;
}(4)原子操作(std::atomic)
对于简单的共享变量(如计数器),使用 atomic 比
mutex 更高效(无需加锁,直接操作内存)。atomic
支持原子的增、减、赋值等操作,线程安全。
#include <thread>
#include <atomic>
#include <iostream>
using namespace std;
atomic<int> g_count = 0; // 原子变量(线程安全)
void Increment() {
for (int i = 0; i < 100000; ++i) {
++g_count; // 原子操作,无需加锁
}
}
int main() {
thread t1(Increment);
thread t2(Increment);
t1.join();
t2.join();
cout << "最终计数:" << g_count << endl; // 正确输出 200000
return 0;
}3. 线程安全与死锁
(1)线程安全的核心原则
- 最小临界区:只对共享资源的操作加锁,避免大面积代码阻塞;
- 避免共享可变状态:尽量使用局部变量,或通过消息传递(而非共享内存)实现线程通信;
- 优先原子操作:简单变量用
atomic,复杂场景用mutex; - 禁止在临界区调用外部接口:外部接口可能再次加锁,导致死锁。
(2)死锁及避免
死锁是多线程编程的噩梦,指两个或多个线程互相等待对方释放锁,导致程序永久阻塞。死锁的 4 个必要条件:
- 互斥:资源只能被一个线程持有;
- 持有并等待:线程持有一个锁,同时等待另一个锁;
- 不可剥夺:线程持有的锁不能被强制剥夺;
- 循环等待:线程间形成循环等待链(如 T1 等 T2 的锁,T2 等 T1 的锁)。
死锁避免方案:
- 按固定顺序加锁:所有线程都按相同的顺序获取多个锁(如先锁 A 再锁 B);
- 超时加锁:用
unique_lock::try_lock_for(),超时后释放已持有锁并重试; - 避免持有多个锁:尽量设计为单个锁,或用
std::lock()同时加多个锁(原子操作,避免部分加锁); - 减少锁的持有时间:获取锁后尽快完成操作,释放锁。
死锁示例(错误):
mutex mutexA;
mutex mutexB;
// 线程1:先锁A再锁B
void Thread1() {
mutexA.lock();
this_thread::sleep_for(chrono::milliseconds(100)); // 让线程2有时间锁B
mutexB.lock(); // 死锁:等待线程2释放B
// 业务逻辑
mutexB.unlock();
mutexA.unlock();
}
// 线程2:先锁B再锁A
void Thread2() {
mutexB.lock();
this_thread::sleep_for(chrono::milliseconds(100)); // 让线程1有时间锁A
mutexA.lock(); // 死锁:等待线程1释放A
// 业务逻辑
mutexA.unlock();
mutexB.unlock();
}修复方案(按固定顺序加锁):
// 线程1和线程2都按“先A后B”的顺序加锁
void Thread1() {
mutexA.lock();
mutexB.lock();
// 业务逻辑
mutexB.unlock();
mutexA.unlock();
}
void Thread2() {
mutexA.lock();
mutexB.lock();
// 业务逻辑
mutexB.unlock();
mutexA.unlock();
}4. 高级多线程技术
(1)线程池
线程池是“预先创建多个线程,复用线程处理任务”的技术,避免频繁创建/销毁线程的开销(线程创建销毁是重量级操作)。核心组件:
- 任务队列:存储待执行的任务;
- 线程池:多个工作线程,循环从任务队列获取任务执行;
- 管理接口:添加任务、关闭线程池。
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <functional>
#include <vector>
#include <atomic>
#include <iostream>
using namespace std;
class ThreadPool {
public:
// 构造函数:创建n个工作线程
explicit ThreadPool(size_t thread_num) : stop_(false) {
for (size_t i = 0; i < thread_num; ++i) {
workers_.emplace_back([this]() {
while (true) {
function<void()> task;
{
unique_lock<mutex> lock(mtx_);
// 等待任务或停止信号
cv_.wait(lock, [this]() { return stop_ || !tasks_.empty(); });
// 若停止且任务队列为空,退出线程
if (stop_ && tasks_.empty()) return;
// 获取任务
task = move(tasks_.front());
tasks_.pop();
}
task(); // 执行任务
}
});
}
}
// 析构函数:关闭线程池
~ThreadPool() {
{
unique_lock<mutex> lock(mtx_);
stop_ = true;
}
cv_.notify_all(); // 唤醒所有工作线程
for (thread& worker : workers_) {
worker.join(); // 等待所有线程结束
}
}
// 添加任务(支持任意函数和参数)
template <typename F, typename... Args>
void AddTask(F&& f, Args&&... args) {
{
unique_lock<mutex> lock(mtx_);
// 禁止在停止后添加任务
if (stop_) throw runtime_error("线程池已关闭,无法添加任务");
// 封装任务(完美转发)
tasks_.emplace([=]() {
forward<F>(f)(forward<Args>(args)...);
});
}
cv_.notify_one(); // 唤醒一个工作线程
}
private:
vector<thread> workers_; // 工作线程列表
queue<function<void()>> tasks_; // 任务队列
mutex mtx_; // 互斥锁
condition_variable cv_; // 条件变量
atomic<bool> stop_; // 停止标志(原子变量,线程安全)
};
// 测试任务:计算a + b
int Add(int a, int b) {
this_thread::sleep_for(chrono::milliseconds(100)); // 模拟任务耗时
return a + b;
}
int main() {
ThreadPool pool(4); // 创建4个线程的线程池
// 添加10个任务
for (int i = 0; i < 10; ++i) {
pool.AddTask([i]() {
int result = Add(i, i * 2);
cout << "线程 " << this_thread::get_id() << " 完成任务:" << i << " + " << i*2 << " = " << result << endl;
});
}
// 等待所有任务完成(这里简单休眠,实际可通过future实现)
this_thread::sleep_for(chrono::seconds(2));
return 0;
}(2)std::future 与
std::promise
future 和 promise
用于线程间数据传递(获取异步任务的返回值):
promise:生产者线程设置值;future:消费者线程获取值(会阻塞直到值被设置)。
#include <thread>
#include <future>
#include <iostream>
using namespace std;
// 生产者线程:计算结果并设置到promise
void Calculate(promise<int> prom) {
this_thread::sleep_for(chrono::seconds(1));
int result = 100 + 200;
prom.set_value(result); // 设置结果
}
int main() {
// 创建promise和future
promise<int> prom;
future<int> fut = prom.get_future();
// 启动线程,传递promise(必须移动,因为promise不可拷贝)
thread t(Calculate, move(prom));
// 获取结果(阻塞直到set_value被调用)
cout << "等待计算结果..." << endl;
int result = fut.get(); // 只能调用一次get()
cout << "计算结果:" << result << endl;
t.join();
return 0;
}(3)std::async
async
是更高层次的异步编程接口,自动创建线程(或复用线程池)执行任务,并返回
future 用于获取结果。无需手动管理线程和
promise。
#include <future>
#include <iostream>
using namespace std;
int Calculate() {
this_thread::sleep_for(chrono::seconds(1));
return 300 + 400;
}
int main() {
// 启动异步任务(默认创建新线程)
future<int> fut = async(Calculate);
cout << "等待异步任务结果..." << endl;
int result = fut.get();
cout << "异步任务结果:" << result << endl;
return 0;
}(4)std::packaged_task
std::packaged_task 是 C++11
引入的标准库模板类,用于包装一个可调用对象(如函数、lambda
表达式、函数对象等),使其能够与 std::future
协同工作。
简单来说,它将一个任务(可调用对象)包装起来,并允许你获取该任务的执行结果,这样你可以在一个线程中执行任务,并在另一个线程中获取其结果。
一个简化的设计参考
template <typename F>
class packaged_task {
public:
packaged_task(F&& f) : task(std::forward<F>(f)), promise(), future(promise.get_future()) {}
void operator()() {
try {
// 调用原始函数,并将结果通过 promise 设置
promise.set_value(task());
} catch (...) {
promise.set_exception(std::current_exception());
}
}
std::future<typename std::result_of<F()>::type> get_future() {
return std::move(future);
}
private:
F task; // 存储原始函数
std::promise<typename std::result_of<F()>::type> promise;
std::future<typename std::result_of<F()>::type> future;
};使用demo:
#include <iostream>
#include <future>
#include <thread>
#include <chrono>
// 一个简单的函数,用于计算整数平方
int square(int x) {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
return x * x;
}
int main() {
// 创建一个 std::packaged_task,包装 square 函数
std::packaged_task<int(int)> task(square);
// 获取关联的 std::future 对象,用于获取任务结果
std::future<int> result = task.get_future();
// 将任务交给一个线程异步执行
std::thread t(std::move(task), 5); // 传递参数5给square函数
t.detach(); // 分离线程
// 在主线程中继续做其他事情
std::cout << "Doing some work in the main thread..." << std::endl;
// 获取任务的返回值,等待任务执行完成
std::cout << "The result of square(5) is: " << result.get() << std::endl;
return 0;
}性能优化
C++ 的核心优势是“高性能”,性能优化是 C++ 开发的核心竞争力。优化的核心原则是“先测量,后优化”(通过 Profiler 定位瓶颈,避免盲目优化)。
1. 代码层优化
(1)减少拷贝,多用移动
拷贝操作(尤其是大对象)会消耗大量内存和 CPU,C++11
后的移动语义(std::move)可避免不必要的拷贝。
#include <vector>
#include <string>
#include <iostream>
using namespace std;
int main() {
vector<string> vec;
string s = "hello world";
// 方式1:拷贝(创建新字符串,效率低)
vec.push_back(s);
cout << "拷贝后 s 是否为空:" << s.empty() << endl; // false
// 方式2:移动(转移所有权,效率高,s变为空)
vec.push_back(move(s));
cout << "移动后 s 是否为空:" << s.empty() << endl; // true
return 0;
}- 优化场景:
- 函数返回大对象时,返回值会自动移动(C++11 后 NRVO 优化);
- 容器添加元素时,对临时对象或不再使用的对象用
move; - 自定义类实现移动构造/移动赋值运算符(Rule of Five)。
(2)避免不必要的内存分配
内存分配(new/malloc)是耗时操作,频繁分配会导致内存碎片和性能下降。
- 优化方案:
- 预分配内存:
vector::reserve()预分配容量,避免频繁扩容; - 对象池:复用对象(如线程池、连接池),减少创建/销毁开销;
- 栈内存优先:小对象用栈内存(局部变量),避免堆内存分配;
- 避免临时对象:如用
emplace_back()替代push_back()(原地构造对象,避免临时对象)。
- 预分配内存:
#include <vector>
#include <string>
using namespace std;
struct Person {
string name;
int age;
Person(string n, int a) : name(n), age(a) {}
};
int main() {
vector<Person> vec;
vec.reserve(100); // 预分配100个元素的容量,避免扩容
// emplace_back:原地构造Person,无需创建临时对象
vec.emplace_back("Tom", 18);
// push_back:创建临时Person,再拷贝/移动到容器
vec.push_back(Person("Jerry", 17));
return 0;
}(3)循环优化
循环是性能热点,优化循环能大幅提升性能:
- 减少循环内的计算:将不变的计算移到循环外;
- 减少循环内的函数调用:函数调用有开销,可内联(
inline)或手动展开; - 缓存友好:按内存顺序访问数据(CPU 缓存是按块加载的,连续访问命中率高);
- 循环展开:减少循环控制开销(适用于循环次数固定的场景)。
// 优化前:循环内重复计算
for (int i = 0; i < vec.size(); ++i) {
// vec.size() 每次循环都要计算(vector的size是O(1),但仍有开销)
}
// 优化后:将size移到循环外
int size = vec.size();
for (int i = 0; i < size; ++i) {
// ...
}
// 缓存友好:连续访问数组(CPU缓存命中率高)
int arr[1000];
for (int i = 0; i < 1000; ++i) {
arr[i] = i; // 连续访问,缓存友好
}
// 非缓存友好:跳跃访问(缓存命中率低)
for (int i = 0; i < 1000; i += 100) {
arr[i] = i; // 跳跃访问,缓存失效频繁
}(4)使用高效的数据结构和算法
- 查找场景:
unordered_map(平均 O(1))优于map(O(log n)),需有序时用map; - 排序场景:
sort(快速排序)优于冒泡排序、插入排序; - 容器选择:根据操作场景选择(如频繁尾部增删用
vector,中间增删用list); - 避免使用低效算法:如嵌套循环(O(n²)),尽量用 O(n) 或 O(n log n) 算法。
2. 编译优化
编译器优化是“零成本”优化,通过编译器参数开启优化,无需修改代码:
- GCC/Clang:
-O1(基础优化)、-O2(常用优化,平衡速度和编译时间)、-O3(极致优化,可能增大二进制体积); - MSVC:
/O1(最小化体积)、/O2(最大化速度)。
编译器优化的核心内容:
- 常量折叠:计算编译期常量(如
3+5直接优化为8); - 死代码消除:删除永远不会执行的代码;
- 循环优化:循环展开、循环变量外提;
- 函数内联:消除函数调用开销;
- 指令重排:优化指令执行顺序,充分利用 CPU 流水线。
3. 内存优化
(1)减少内存碎片
内存碎片是频繁分配/释放不同大小的堆内存导致的,会浪费内存并降低分配效率:
- 用
vector替代多个独立的new:vector是连续内存,减少碎片; - 使用内存池:预先分配一块大内存,按需分配小块内存,释放时归还给内存池;
- 对齐内存:
alignas指定内存对齐(CPU 访问对齐的内存更快)。
// 指定内存对齐为8字节
struct alignas(8) Data {
int a;
char b;
};
// 内存池示例(简化)
class MemoryPool {
private:
char* pool_;
size_t size_;
size_t used_;
public:
MemoryPool(size_t size) : size_(size), used_(0) {
pool_ = new char[size];
}
~MemoryPool() {
delete[] pool_;
}
void* Allocate(size_t alloc_size) {
if (used_ + alloc_size > size_) return nullptr;
void* ptr = pool_ + used_;
used_ += alloc_size;
return ptr;
}
void Deallocate(void* ptr) {
// 简化实现:不实际释放,仅重置used_(适用于批量释放场景)
if (ptr == pool_) used_ = 0;
}
};(2)缓存优化
CPU 缓存是提升内存访问速度的关键(缓存速度比内存快 10-100 倍),优化缓存命中率:
- 数据局部性:将频繁访问的数据放在一起(如结构体成员按访问频率排序);
- 避免缓存失效:减少对同一缓存行的并发写(伪共享);
- 预取数据:
__builtin_prefetch(GCC)或_mm_prefetch(MSVC)手动预取数据到缓存。
工程化规范与实践
工程化开发的核心是“让代码可维护、可协作、可扩展”,涵盖编码规范、版本控制、构建系统、测试等多个方面。
1. 编码规范
编码规范是团队协作的基础,统一的风格能减少沟通成本,提升代码可读性。推荐遵循:
- Google C++ 编码规范:最流行的规范,涵盖命名、格式、注释、特性使用等;
- ISO C++ Core Guidelines:C++ 标准委员会推荐的指南,聚焦安全性、高效性和可维护性。
核心规范要点
- 命名:
- 类/结构体/枚举:大驼峰(
PersonInfo、LogLevel); - 函数/变量:小驼峰(
calculateSum、userName)或下划线(calculate_sum、user_name); - 常量/宏:全大写+下划线(
MAX_QUEUE_SIZE、LOG_DEBUG); - 私有成员:后缀加下划线(
name_、age_)。
- 类/结构体/枚举:大驼峰(
- 格式:
- 缩进:4 个空格(避免 Tab,跨编辑器一致);
- 行宽:不超过 80-120 字符;
- 大括号:函数/类的左大括号单独一行,循环/条件的左大括号紧跟语句。
- 注释:
- 类/函数:用 Doxygen 风格注释(说明功能、参数、返回值、异常);
- 复杂逻辑:添加行注释,说明设计思路;
- 避免冗余注释(如“i++:i自增1”)。
- 特性使用:
- 禁用裸
new/delete,用智能指针; - 禁用宏,用
constexpr或inline函数; - 优先使用标准库,避免重复造轮子;
- 避免使用
void*、裸数组,用std::any、std::vector。
- 禁用裸