多线程是并发编程的核心技术,它允许程序在同一进程内创建多个执行流(线程),这些线程共享进程的资源(如内存空间、文件描述符等),但拥有独立的执行路径。在C++中,自C++11起,标准库通过<thread>
头文件提供了跨平台的多线程支持,极大简化了多线程编程。
一、多线程基础:线程的创建与管理
1. 线程的本质与优势
- 线程(Thread):进程内的一个执行单元,是CPU调度的基本单位。一个进程可以包含多个线程,线程间共享进程的地址空间和资源。
- 优势:
- 提高CPU利用率(尤其多核环境下,线程可并行执行)。
- 提升程序响应性(如UI线程与后台计算线程分离)。
- 简化复杂任务的拆分(如并行处理数据)。
2. C++中创建线程:std::thread
std::thread
是C++标准库的线程类,通过它可创建新线程。线程创建后,需通过join()
或detach()
管理其生命周期。
核心操作:
- 构造函数:传入可调用对象(函数、lambda、仿函数等)作为线程入口。
join()
:阻塞当前线程,等待目标线程执行完毕后再继续。detach()
:将线程与std::thread
对象分离,线程后台运行,由系统回收资源。joinable()
:判断线程是否可join
或detach
(已处理过的线程返回false
)。
我来详细理解一下join和detach的区别和定义哈:join就是从线程创建执行位置,从主线中断去执行该子线程中的内容,而join的目的就是用来阻塞主线程的执行,join之后的命令,必须要该join对应的线程结束后,才会执行。
而detach,emm,感觉更难理解了一些, 我目前的理解是,detach执行后,子线程会后台执行,会和主线程并行执行,但我不理解emmm,他应该放的位置,但我知道,尽量放在主程序终止前,且建议尽早调用,确保子线程访问的资源比子线程生命周期长。
示例1:基本线程创建与join()
#include <iostream>
#include <thread> // 多线程核心头文件
// 线程执行的函数
void thread_func(int id) {
std::cout << "Thread " << id << " is running\n";
}
int main() {
// 创建线程:传入函数和参数
std::thread t1(thread_func, 1); // t1 对应线程1
std::thread t2(thread_func, 2); // t2 对应线程2
// 等待线程执行完毕(必须调用join或detach,否则程序异常终止)
t1.join(); // 主线程等待t1结束
t2.join(); // 主线程等待t2结束
std::cout << "Main thread ends\n";
return 0;
}
输出(顺序可能因调度不同而变化):
Thread 1 is running
Thread 2 is running
Main thread ends
示例2:detach()
的使用(后台线程)
#include <iostream>
#include <thread>
#include <chrono> // 用于时间相关操作
void background_task() {
for (int i = 0; i < 3; ++i) {
std::cout << "Background working... " << i << "\n";
std::this_thread::sleep_for(std::chrono::seconds(1)); // 线程休眠1秒
}
}
int main() {
std::thread t(background_task);
t.detach(); // 线程分离,后台运行
// 主线程休眠2秒,确保能看到后台线程输出
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "Main thread exits\n";
return 0;
}
说明:
detach()
后,线程独立运行,std::thread
对象不再管理它。若主线程提前结束,后台线程可能被强制终止(如上例中后台线程的第3次输出可能无法打印)。
注意:
- 若
std::thread
对象销毁时未调用join()
或detach()
,其析构函数会调用std::terminate()
终止程序。 - 线程入口函数的参数会被拷贝到线程内部,若需传递引用,需用
std::ref()
包装(但需确保引用对象生命周期长于线程)。
二、共享资源与数据竞争
多线程的核心问题是共享资源访问。当多个线程同时读写同一资源(如全局变量、堆内存)时,可能出现数据竞争(Data Race),导致结果不可预测。
1. 数据竞争示例
#include <iostream>
#include <thread>
int shared_count = 0; // 共享资源
// 线程函数:对共享变量自增10000次
void increment() {
for (int i = 0; i < 10000; ++i) {
shared_count++; // 危险:非原子操作,可能引发数据竞争
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
// 预期结果:20000,但实际可能小于20000(因数据竞争)
std::cout << "Final count: " << shared_count << "\n";
return 0;
}
问题原因:
shared_count++
看似简单,实际包含三步操作(读取→修改→写入)。若两个线程同时读取到相同值,修改后写入会覆盖彼此的结果(如都读取到100,都修改为101,最终结果少加1)。
2. 解决数据竞争:同步机制
同步机制确保临界区(Critical Section,即访问共享资源的代码段) 互斥执行。C++提供多种同步工具,核心是锁和原子操作。
(1)互斥锁(std::mutex
)
用std::mutex
保证同一时间只有一个线程进入临界区:
#include <iostream>
#include <thread>
#include <mutex> // 互斥锁头文件
std::mutex mtx; // 全局互斥锁
int shared_count = 0;
void safe_increment() {
for (int i = 0; i < 10000; ++i) {
mtx.lock(); // 获取锁(若被占用则阻塞)
shared_count++; // 临界区:安全修改共享资源
mtx.unlock(); // 释放锁
}
}
int main() {
std::thread t1(safe_increment);
std::thread t2(safe_increment);
t1.join();
t2.join();
std::cout << "Final count: " << shared_count << "\n"; // 必然是20000
return 0;
}
优化:用std::lock_guard
(RAII)自动管理锁的释放,避免遗漏unlock()
:
void safer_increment() {
for (int i = 0; i < 10000; ++i) {
std::lock_guard<std::mutex> lock(mtx); // 构造时锁,析构时自动解锁
shared_count++;
}
}
(2)原子操作(std::atomic
)
对于简单的读写操作(如计数器),std::atomic
提供硬件级别的原子性,无需锁,效率更高:
#include <iostream>
#include <thread>
#include <atomic> // 原子操作头文件
std::atomic<int> atomic_count(0); // 原子变量(默认初始化为0)
void atomic_increment() {
for (int i = 0; i < 10000; ++i) {
atomic_count++; // 原子操作,无数据竞争
}
}
int main() {
std::thread t1(atomic_increment);
std::thread t2(atomic_increment);
t1.join();
t2.join();
std::cout << "Atomic count: " << atomic_count << "\n"; // 必然是20000
return 0;
}
说明:
std::atomic
支持基本数据类型(int
、long
、指针等),其操作由CPU指令直接保证原子性,无需上下文切换,性能优于锁。
三、线程间通信:条件变量
多线程除了共享资源,还需协作完成任务(如"生产者-消费者"模型)。std::condition_variable
是线程间通信的核心工具,用于线程等待某个条件成立。
生产者-消费者模型示例
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>
const int MAX_QUEUE_SIZE = 5; // 队列最大容量
std::queue<int> data_queue; // 共享队列(生产者放入,消费者取出)
std::mutex mtx; // 保护队列的锁
std::condition_variable cv; // 条件变量
// 生产者:生成数据并放入队列
void producer() {
for (int i = 1; i <= 10; ++i) { // 生成10个数据
std::unique_lock<std::mutex> lock(mtx);
// 等待队列有空闲位置(若队列满则阻塞)
cv.wait(lock, []{ return data_queue.size() < MAX_QUEUE_SIZE; });
// 放入数据
data_queue.push(i);
std::cout << "Produced: " << i << " (Queue size: " << data_queue.size() << ")\n";
lock.unlock();
cv.notify_one(); // 通知消费者有新数据
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟生产耗时
}
}
// 消费者:从队列取出数据并处理
void consumer() {
for (int i = 1; i <= 10; ++i) { // 消费10个数据
std::unique_lock<std::mutex> lock(mtx);
// 等待队列非空(若队列为空则阻塞)
cv.wait(lock, []{ return !data_queue.empty(); });
// 取出数据
int data = data_queue.front();
data_queue.pop();
std::cout << "Consumed: " << data << " (Queue size: " << data_queue.size() << ")\n";
lock.unlock();
cv.notify_one(); // 通知生产者有空闲位置
std::this_thread::sleep_for(std::chrono::milliseconds(150)); // 模拟消费耗时
}
}
int main() {
std::thread prod(producer);
std::thread cons(consumer);
prod.join();
cons.join();
return 0;
}
核心逻辑:
- 生产者满队列时阻塞,等待消费者取走数据后唤醒。
- 消费者空队列时阻塞,等待生产者放入数据后唤醒。
cv.wait(lock, predicate)
:原子地释放锁并阻塞,被唤醒后重新获取锁并检查条件(防止虚假唤醒)。
四、线程局部存储(thread_local
)
多线程中,若每个线程需要独立的变量副本(不共享),可使用thread_local
关键字,其生命周期与线程一致。
示例:线程私有变量
#include <iostream>
#include <thread>
// 线程局部变量:每个线程有独立副本
thread_local int thread_id = 0;
void print_id(int id) {
thread_id = id; // 为当前线程的副本赋值
for (int i = 0; i < 3; ++i) {
std::cout << "Thread " << thread_id << " iteration " << i << "\n";
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
}
int main() {
std::thread t1(print_id, 1);
std::thread t2(print_id, 2);
t1.join();
t2.join();
return 0;
}
输出(片段):
Thread 1 iteration 0
Thread 2 iteration 0
Thread 1 iteration 1
Thread 2 iteration 1
...
说明:
thread_id
在两个线程中是独立的,修改一个线程的副本不影响另一个。
五、多线程常见问题与解决方案
1. 死锁(Deadlock)
定义:两个或多个线程互相等待对方释放锁,导致永久阻塞。
示例:
std::mutex mtx1, mtx2;
// 线程1:先锁mtx1,再等mtx2
void thread1() {
mtx1.lock();
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 给线程2抢锁机会
mtx2.lock(); // 等待mtx2,此时线程2已持有mtx2并等待mtx1,死锁!
// ...
mtx2.unlock();
mtx1.unlock();
}
// 线程2:先锁mtx2,再等mtx1
void thread2() {
mtx2.lock();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
mtx1.lock(); // 等待mtx1,死锁!
// ...
mtx1.unlock();
mtx2.unlock();
}
解决方法:
- 按固定顺序加锁(如所有线程先锁地址小的锁)。
- 用
std::lock
同时获取多个锁(原子操作,避免部分加锁):void safe_thread() { std::lock(mtx1, mtx2); // 原子获取两个锁,无死锁风险 std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock); // 接管已获取的锁 std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock); // ... }
2. 活锁(Livelock)
定义:线程不断响应对方的动作,却无法推进工作(如两个线程互相释放锁并立即重试,导致无限循环)。
解决方法:
- 引入随机延迟(减少冲突概率)。
- 优先级机制(让一个线程优先获取资源)。
3. 饥饿(Starvation)
定义:某些线程长期无法获取资源(如低优先级线程被高优先级线程持续抢占)。
解决方法:
- 避免长时间持有锁。
- 合理设置线程优先级(需谨慎,可能引入新问题)。
六、线程池(Thread Pool)
频繁创建/销毁线程会带来性能开销(线程上下文切换、资源分配)。线程池预先创建一批线程,重复利用它们执行任务,降低开销。
简单线程池实现
#include <iostream>
#include <thread>
#include <vector>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <atomic>
class ThreadPool {
private:
std::vector<std::thread> workers; // 工作线程
std::queue<std::function<void()>> tasks; // 任务队列
std::mutex mtx; // 保护任务队列
std::condition_variable cv; // 通知线程有任务
std::atomic<bool> stop; // 线程池停止标志
public:
// 构造函数:创建n个工作线程
ThreadPool(size_t n) : stop(false) {
for (size_t i = 0; i < n; ++i) {
workers.emplace_back([this] {
while (!stop) { // 循环等待任务
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mtx);
// 等待任务或停止信号
cv.wait(lock, [this] { return stop || !tasks.empty(); });
if (stop && tasks.empty()) return; // 停止且无任务,退出线程
task = std::move(tasks.front()); // 取出任务
tasks.pop();
}
task(); // 执行任务
}
});
}
}
// 析构函数:停止所有线程
~ThreadPool() {
stop = true;
cv.notify_all(); // 唤醒所有等待的线程
for (auto& worker : workers) {
worker.join(); // 等待线程结束
}
}
// 添加任务到队列
template<class F>
void enqueue(F&& f) {
{
std::unique_lock<std::mutex> lock(mtx);
tasks.emplace(std::forward<F>(f)); // 放入任务
}
cv.notify_one(); // 通知一个线程执行任务
}
};
// 使用示例
int main() {
ThreadPool pool(3); // 创建3个线程的线程池
// 添加5个任务
for (int i = 0; i < 5; ++i) {
pool.enqueue([i] {
std::cout << "Task " << i << " executed by thread "
<< std::this_thread::get_id() << "\n";
std::this_thread::sleep_for(std::chrono::seconds(1));
});
}
return 0; // 析构函数自动停止线程池
}
说明:
- 线程池创建时初始化工作线程,线程循环等待任务队列。
- 任务通过
enqueue
添加到队列,线程池自动调度空闲线程执行。 - 析构时停止线程池,确保所有线程安全退出。
七、多线程最佳实践
- 最小化共享状态:减少线程间共享资源,降低同步复杂度(如用消息传递替代共享内存)。
- 优先使用原子操作:简单操作(计数、标志位)用
std::atomic
,避免锁的开销。 - 用RAII管理锁:
lock_guard
/unique_lock
确保锁正确释放,避免死锁。 - 避免长时间持有锁:仅在临界区保留锁,完成后立即释放。
- 测试与调试:多线程bug难以复现,可使用工具(如Valgrind的Helgrind)检测数据竞争。
总结
C++多线程编程通过std::thread
、std::mutex
、std::condition_variable
等工具,提供了完整的并发解决方案。核心挑战是共享资源同步和线程协作,需理解锁、原子操作、条件变量等机制,并警惕死锁、数据竞争等问题。合理使用线程池可提升性能,而最小化共享状态是编写可靠多线程代码的关键。