1. 为什么需要多线程编程
想象你正在餐厅吃饭,服务员需要同时处理多个任务:点单、上菜、结账。如果只有一个服务员按顺序处理这些请求,顾客等待时间会很长。多线程就像雇佣多个服务员,让餐厅服务效率成倍提升。
在计算机世界中,多线程能让程序同时处理多个任务。比如浏览器可以一边下载文件一边渲染页面,游戏可以在处理物理引擎的同时响应用户输入。C++11之前,开发者需要使用平台特定的API(如Windows的CreateThread或Linux的pthread),现在有了标准化的std::thread,代码可以跨平台运行。
我曾在图像处理项目中遇到性能瓶颈,单线程处理4000x3000像素图片需要3秒,改用4个线程后时间缩短到0.8秒。这就是多线程的魅力——合理利用CPU多核能力。
2. std::thread基础用法
2.1 创建第一个线程
#include <iostream> #include <thread> void hello() { std::cout << "Hello from thread!\n"; } int main() { std::thread t(hello); // 线程立即启动 t.join(); // 等待线程结束 return 0; }这个简单例子展示了线程创建的核心步骤:
- 包含 头文件
- 定义线程函数(这里用普通函数hello)
- 构造std::thread对象时传入函数
- 调用join等待线程结束
新手常犯的错误是忘记join或detach,这会导致程序终止。记住:每个线程对象在销毁前必须调用join()或detach()。
2.2 三种创建线程的方式
除了普通函数,std::thread还支持多种可调用对象:
Lambda表达式(推荐)
std::thread t([](){ std::cout << "Lambda thread\n"; });函数对象
struct Task { void operator()(int n) const { std::cout << "Processing " << n << "\n"; } }; std::thread t(Task(), 42);类成员函数
class Worker { public: void run(int id) { std::cout << "Worker " << id << " running\n"; } }; Worker w; std::thread t(&Worker::run, &w, 1);实际项目中,我更喜欢用lambda表达式,因为它可以直接捕获局部变量,代码更紧凑。函数对象适合复杂任务,而成员函数方式则适合面向对象设计。
3. 线程参数传递技巧
3.1 值传递与引用传递
默认情况下,参数会被拷贝到线程内部:
void modify(int n) { n = 10; } int main() { int x = 5; std::thread t(modify, x); t.join(); std::cout << x; // 输出5,x未被修改 }要传递引用必须使用std::ref:
void modify(int& n) { n = 10; } int main() { int x = 5; std::thread t(modify, std::ref(x)); t.join(); std::cout << x; // 输出10 }3.2 移动语义的应用
对于不可复制的对象(如unique_ptr),可以使用移动语义:
std::unique_ptr<int> ptr(new int(42)); std::thread t([](std::unique_ptr<int> p){ std::cout << *p << "\n"; }, std::move(ptr));我曾在一个网络项目中用这种方式将套接字所有权转移给工作线程,避免了复杂的锁机制。
4. 线程生命周期管理
4.1 join与detach的抉择
- join():阻塞当前线程直到目标线程完成
- detach():让线程在后台自主运行
std::thread t(heavy_task); // 方案1:等待任务完成 t.join(); // 方案2:分离线程(风险!) t.detach();经验法则:除非有特殊需求,否则优先使用join。detach的线程就像脱缰野马,难以追踪和管理。我曾调试过一个detach线程访问已销毁对象的bug,花了整整两天。
4.2 使用RAII自动管理
为避免异常导致线程未join,可以实现一个守卫类:
class ThreadGuard { std::thread& t; public: explicit ThreadGuard(std::thread& t_) : t(t_) {} ~ThreadGuard() { if(t.joinable()) t.join(); } // 禁止拷贝 ThreadGuard(const ThreadGuard&)=delete; ThreadGuard& operator=(const ThreadGuard&)=delete; }; void foo() { std::thread t([]{ /*...*/ }); ThreadGuard g(t); // 即使这里抛出异常,线程也会被正确join }5. 线程同步机制
5.1 mutex基础用法
std::mutex mtx; int shared_data = 0; void safe_increment() { std::lock_guard<std::mutex> lock(mtx); ++shared_data; }lock_guard在构造时加锁,析构时自动解锁,即使发生异常也能保证锁被释放。这是我最常用的RAII锁类型。
5.2 避免死锁的几种方法
- 按固定顺序上锁
std::mutex mtx1, mtx2; // 线程A std::lock(mtx1, mtx2); // 同时锁定多个互斥量 std::lock_guard<std::mutex> lk1(mtx1, std::adopt_lock); std::lock_guard<std::mutex> lk2(mtx2, std::adopt_lock); // 线程B同样顺序 std::lock(mtx1, mtx2); // ...- 使用std::unique_lock
std::mutex mtx; std::unique_lock<std::mutex> lock(mtx, std::defer_lock); lock.lock(); // 可以手动控制加锁时机 // ... lock.unlock(); // 也可以提前解锁- 设置超时
std::timed_mutex mtx; if(mtx.try_lock_for(std::chrono::milliseconds(100))) { // 获取锁成功 mtx.unlock(); }在实际项目中,我曾遇到过一个经典死锁场景:线程A持有锁1请求锁2,线程B持有锁2请求锁1。通过统一锁的获取顺序解决了这个问题。
6. 高级线程通信
6.1 condition_variable使用模式
生产者-消费者模型的典型实现:
std::mutex mtx; std::queue<int> data_queue; std::condition_variable data_cond; void producer() { while(true) { std::unique_lock<std::mutex> lock(mtx); data_queue.push(42); data_cond.notify_one(); lock.unlock(); std::this_thread::sleep_for(std::chrono::seconds(1)); } } void consumer() { while(true) { std::unique_lock<std::mutex> lock(mtx); data_cond.wait(lock, []{ return !data_queue.empty(); }); int data = data_queue.front(); data_queue.pop(); lock.unlock(); process(data); } }condition_variable的wait操作会自动释放锁并阻塞,直到被唤醒后重新获取锁。这种机制比轮询高效得多。
6.2 future/promise模式
std::promise<int> prom; std::future<int> fut = prom.get_future(); std::thread t([](std::promise<int> p){ p.set_value(42); // 设置值 }, std::move(prom)); std::cout << fut.get(); // 获取结果这个模式非常适合需要获取异步任务结果的场景。我在一个分布式计算项目中用它来收集各个工作节点的计算结果。
7. 性能优化技巧
7.1 原子操作替代锁
std::atomic<int> counter(0); void safe_increment() { ++counter; // 原子操作,无需锁 }对于简单数据类型,原子操作比互斥锁快10-100倍。但要注意:原子操作不是万能的,复杂操作仍需锁。
7.2 线程局部存储
thread_local int thread_spec = 0; void use_tls() { ++thread_spec; // 每个线程有自己的副本 }适合存储线程特定的状态,比如随机数生成器、数据库连接等。我曾用它实现线程安全的日志系统,每个线程有自己的日志缓冲区。
7.3 线程池模式
避免频繁创建销毁线程的开销:
class ThreadPool { std::vector<std::thread> workers; std::queue<std::function<void()>> tasks; // ... 同步原语 public: void enqueue(std::function<void()> task) { // 将任务加入队列并通知工作线程 } // ... };线程池是实际项目中的标配。根据我的测试,对于短任务,线程池比每次新建线程快20倍以上。
8. 常见陷阱与解决方案
- 悬垂引用:线程使用已销毁的局部变量
void danger() { int x = 42; std::thread t([&x]{ std::cout << x; }); // 错误! t.detach(); } // x被销毁,线程可能还在运行- 异常安全:线程函数抛出异常未捕获
void safe_thread() noexcept try { // 线程代码 } catch(...) { // 记录日志 }- 资源泄漏:忘记join或detach
std::thread t([]{}); if(t.joinable()) t.join(); // 必须检查- 虚假共享:多个线程频繁修改同一缓存行的不同变量
struct alignas(64) CacheLine { // 64字节对齐 int data1; int data2; };在多线程开发中,这些陷阱我都踩过。最惨痛的一次是虚假共享导致性能下降80%,最后通过内存对齐解决。