写点什么

玩转 C++11 多线程:让你的程序飞起来的 std::thread 终极指南

  • 2025-05-22
    福建
  • 本文字数:6807 字

    阅读完需:约 22 分钟

你还在为 C++ 多线程编程发愁吗?别担心,今天咱们就用大白话彻底搞定 std::thread!看完这篇,保证你对 C++11 多线程的理解从"一脸懵逼"变成"原来如此"!


前言:为啥要学多线程?


想象一下,你正在厨房做饭。如果你是单线程工作,那就只能先切菜,切完再炒菜,炒完再煮汤...一项一项按顺序来。但现实中的你肯定是多线程操作啊:锅里炒着菜,同时旁边的电饭煲在煮饭,热水壶在烧水,也许你还能同时看看手机...这就是多线程的威力!


在程序世界里,多线程就像多了几个"分身",可以同时处理不同的任务,充分利用多核 CPU 的性能,让程序跑得飞快。特别是现在谁的电脑不是多核啊,不用多线程简直是浪费资源!


C++11 标准终于给我们带来了官方的多线程支持——std::thread,从此不用再依赖操作系统特定的 API 或第三方库,写多线程程序方便多了!


第一步:创建你的第一个线程


好,闲话少说,直接上代码看看怎么创建一个线程:


#include <iostream>#include <thread>
// 这是我们要在新线程中执行的函数void hello_thread() { std::cout << "哈喽,我是一个新线程!" << std::endl;}
int main() { // 创建一个执行hello_thread函数的线程 std::thread t(hello_thread);
// 主线程打个招呼 std::cout << "主线程:我正在等一个线程干活..." << std::endl;
// 等待线程完成 t.join();
std::cout << "所有线程都结束了,程序退出!" << std::endl; return 0;}
复制代码


输出结果可能是:


主线程:我正在等一个线程干活...哈喽,我是一个新线程!所有线程都结束了,程序退出!
复制代码


或者是:


哈喽,我是一个新线程!主线程:我正在等一个线程干活...所有线程都结束了,程序退出!
复制代码


咦?为啥输出顺序不固定?因为两个线程是并发执行的,谁先打印完全看 CPU 的心情!这就是多线程的特点——不确定性。


代码解析:


创建线程超简单,就一行代码:std::thread t(hello_thread);。线程一创建就立刻开始执行了,就像放出去的炮弹,发射了就收不回来了。


t.join() 是啥意思呢?它相当于说:"主线程,你等等这个新线程,等它干完活再继续"。如果没有这行,主线程可能提前结束,程序就崩溃了!


给线程传参数


线程不能只会喊"哈喽"吧?我们得给它点实际任务,还得告诉它一些参数。传参数超简单:


#include <iostream>#include <thread>#include <string>
void greeting(std::string name, int times) { for (int i = 0; i < times; i++) { std::cout << "你好," << name << "!这是第 " << (i+1) << " 次问候!" << std::endl; }}
int main() { // 创建线程并传递参数 std::thread t(greeting, "张三", 3);
std::cout << "主线程:我让线程去问候张三了..." << std::endl;
// 等待线程完成 t.join();
std::cout << "问候完毕!" << std::endl; return 0;}
复制代码


输出结果:


主线程:我让线程去问候张三了...你好,张三!这是第 1 次问候!你好,张三!这是第 2 次问候!你好,张三!这是第 3 次问候!问候完毕!
复制代码


传参就像普通函数调用一样,直接在线程构造函数后面加参数就行。但是有个坑:参数是"拷贝"到线程中的,所以小心对象的复制开销!


用 Lambda 表达式创建线程


每次都要单独写个函数太麻烦了,有没有简单方法?有啊,用 Lambda 表达式!


#include <iostream>#include <thread>
int main() { // 使用Lambda表达式创建线程 std::thread t([]() { std::cout << "我是Lambda创建的线程,帅不帅?" << std::endl; for (int i = 5; i > 0; i--) { std::cout << "倒计时: " << i << std::endl; } });
std::cout << "主线程:Lambda线程正在倒计时..." << std::endl;
t.join();
std::cout << "倒计时结束!" << std::endl; return 0;}
复制代码


Lambda 表达式就像一个临时小函数,用完就扔,方便得很!特别适合那种只用一次的简单逻辑。


多线程通信的坑:数据竞争


多线程编程最大的坑就是多个线程同时访问同一数据时会出现"数据竞争"。来看个例子:


#include <iostream>#include <thread>#include <vector>
int counter = 0; // 共享的计数器
void increment_counter(int times) { for (int i = 0; i < times; i++) { counter++; // 危险操作!多线程同时修改 }}
int main() { std::vector<std::thread> threads;
// 创建5个线程,每个线程将counter增加10000次 for (int i = 0; i < 5; i++) { threads.push_back(std::thread(increment_counter, 10000)); }
// 等待所有线程完成 for (auto& t : threads) { t.join(); }
std::cout << "理论上counter应该等于:" << 5 * 10000 << std::endl; std::cout << "实际上counter等于:" << counter << std::endl;
return 0;}
复制代码


输出可能是:


理论上counter应该等于:50000实际上counter等于:42568
复制代码


咦?怎么少了那么多?因为 counter++ 看起来是一条语句,但实际上分三步:读取 counter 的值、加 1、写回 counter。当多个线程同时执行这个操作,就会互相"踩踏",导致最终结果小于预期。


这就是臭名昭著的数据竞争问题,解决方法有互斥锁、原子操作等,后面会讲。


线程管理的基本操作


1. join() - 等待线程完成


我们已经见过 join() 了,它会阻塞当前线程,直到目标线程执行完毕。


std::thread t(some_function);t.join(); // 等待t完成
复制代码


2. detach() - 让线程"自生自灭"


有时候,我们启动一个线程后不想等它了,可以用 detach() 让它独立运行:


std::thread t(background_task);t.detach(); // 线程在后台独立运行std::cout << "主线程不管子线程了,继续自己的事" << std::endl;
复制代码


detach 后的线程称为"分离线程"或"守护线程",它会在后台默默运行,直到自己的任务完成。但要小心:如果主程序结束了,这些分离线程会被强制终止!


3. joinable() - 检查线程是否可等待


在 join 之前,最好检查一下线程是否可以被等待:


std::thread t(some_function);// ... 一些代码 ...if (t.joinable()) {    t.join();}
复制代码


这避免了对已经 join 或 detach 过的线程再次操作,否则会崩溃。


防止忘记 join:RAII 风格的线程包装器


C++的经典模式:用对象的生命周期管理资源。我们可以创建一个线程包装器,在析构时自动 join:


#include <iostream>#include <thread>
class thread_guard {private:std::thread& t;
public:// 构造函数,接收线程引用explicit thread_guard(std::thread& t_) : t(t_) {}
// 析构函数,自动join线程~thread_guard() { if (t.joinable()) { t.join(); }}
// 禁止复制thread_guard(const thread_guard&) = delete;thread_guard& operator=(const thread_guard&) = delete;};
void some_function() { std::cout << "线程工作中..." << std::endl;}
int main() { std::thread t(some_function); thread_guard g(t); // 创建守卫对象
// 即使这里抛出异常,thread_guard的析构函数也会被调用,确保t被join std::cout << "主线程继续工作..." << std::endl;
return 0; // 函数结束,g被销毁,自动调用t.join()}
复制代码


这样即使发生异常,或者开发者忘记手动 join,线程也会被正确等待,避免程序崩溃。


线程间的互斥:mutex


前面说到数据竞争问题,最常用的解决方案是互斥锁(mutex):


#include <iostream>#include <thread>#include <mutex>#include <vector>
int counter = 0;std::mutex counter_mutex; // 保护counter的互斥锁
void safe_increment(int times) { for (int i = 0; i < times; i++) { counter_mutex.lock(); // 锁定互斥锁 counter++; // 安全操作 counter_mutex.unlock(); // 解锁 }}
int main() { std::vector<std::thread> threads;
// 创建5个线程 for (int i = 0; i < 5; i++) { threads.push_back(std::thread(safe_increment, 10000)); }
// 等待所有线程 for (auto& t : threads) { t.join(); }
std::cout << "现在counter正确等于:" << counter << std::endl; return 0;}
复制代码


输出:


现在counter正确等于:50000
复制代码


太好了!结果正确了。但这样手动 lock/unlock 很容易出错,如果忘记 unlock 或者发生异常,就会死锁。所以更推荐使用 RAII 风格的std::lock_guard


void better_safe_increment(int times) {    for (int i = 0; i < times; i++) {        std::lock_guard<std::mutex> lock(counter_mutex); // 自动锁定和解锁        counter++;    }}
复制代码


lock_guard在构造时锁定互斥锁,在析构时自动解锁,无论是正常退出还是异常退出都能保证互斥锁被释放。


高级话题:条件变量


线程间的同步不只有互斥,有时我们需要一个线程等待某个条件满足。条件变量就是干这个的:


#include <iostream>#include <thread>#include <mutex>#include <condition_variable>#include <queue>
std::queue<int> data_queue; // 共享的数据队列std::mutex queue_mutex;std::condition_variable data_cond;
// 生产者线程void producer() { for (int i = 0; i < 5; i++) { { std::lock_guard<std::mutex> lock(queue_mutex); data_queue.push(i); // 添加数据 std::cout << "生产了数据: " << i << std::endl; } // 锁在这里释放
data_cond.notify_one(); // 通知一个等待的消费者 std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 稍微等一下 }}
// 消费者线程void consumer() { while (true) { std::unique_lock<std::mutex> lock(queue_mutex); // 等待队列有数据(避免虚假唤醒) data_cond.wait(lock, [] { return !data_queue.empty(); });
// 取出并处理数据 int value = data_queue.front(); data_queue.pop();
std::cout << "消费了数据: " << value << std::endl;
if (value == 4) break; // 收到最后一个数据后退出 }}
int main() { std::thread prod(producer); std::thread cons(consumer);
prod.join(); cons.join();
std::cout << "所有数据都生产和消费完毕!" << std::endl; return 0;}
复制代码


这个例子展示了经典的"生产者-消费者"模式:生产者往队列里放数据,消费者从队列里取数据。条件变量确保消费者不会在队列为空时尝试取数据。


线程与异常安全


在多线程程序中处理异常尤为重要。如果线程执行时抛出异常,且没被捕获,整个程序会直接崩溃!以下是安全处理方式:


#include <iostream>#include <thread>#include <exception>
void function_that_throws() { throw std::runtime_error("故意抛出的异常!");}
void thread_function() { try { function_that_throws(); } catch (const std::exception& e) { std::cout << "线程捕获到异常: " << e.what() << std::endl; }}
int main() { std::thread t(thread_function); t.join();
std::cout << "程序正常结束" << std::endl; return 0;}
复制代码


输出:


线程捕获到异常: 故意抛出的异常!程序正常结束
复制代码


记住:每个线程都有自己独立的调用栈,异常不会跨线程传播!在哪个线程抛出,就必须在哪个线程捕获。


实用技巧


1. 获取线程 ID


每个线程都有唯一的 ID,用于标识:


#include <iostream>#include <thread>
void print_id() { std::cout << "线程ID: " << std::this_thread::get_id() << std::endl;}
int main() { std::thread t1(print_id); std::thread t2(print_id);
std::cout << "主线程ID: " << std::this_thread::get_id() << std::endl; std::cout << "t1的ID: " << t1.get_id() << std::endl; std::cout << "t2的ID: " << t2.get_id() << std::endl;
t1.join(); t2.join();
return 0;}
复制代码


2. 线程休眠


有时需要让线程暂停一会儿:


#include <iostream>#include <thread>#include <chrono>
void sleepy_thread() { std::cout << "我要睡觉了..." << std::endl; std::this_thread::sleep_for(std::chrono::seconds(2)); std::cout << "睡醒了!" << std::endl;}
int main() { std::thread t(sleepy_thread); t.join(); return 0;}
复制代码


3. 获取 CPU 核心数


为了根据 CPU 核心优化线程数量:


#include <iostream>#include <thread>
int main() { unsigned int num_cores = std::thread::hardware_concurrency(); std::cout << "你的CPU有 " << num_cores << " 个硬件线程(核心)" << std::endl;
// 根据核心数创建线程 unsigned int num_threads = num_cores; std::cout << "将创建 " << num_threads << " 个线程以充分利用CPU" << std::endl;
return 0;}
复制代码


实际案例:并行图像处理


来个实际应用案例:用多线程加速图像处理。这里我们简化为操作一个二维数组:


#include <iostream>#include <thread>#include <vector>#include <algorithm>#include <chrono>
// 模拟图像处理函数void process_image_part(std::vector<std::vector<int>>& image, int start_row, int end_row) { for (int i = start_row; i < end_row; i++) { for (int j = 0; j < image[i].size(); j++) { // 模拟复杂处理,例如图像模糊 image[i][j] = (image[i][j] + 10) * 2; // 模拟耗时操作 std::this_thread::sleep_for(std::chrono::microseconds(1)); } }}
int main() { // 创建模拟图像 (1000x1000) std::vector<std::vector<int>> image(1000, std::vector<int>(1000, 5));
// 获取CPU核心数 unsigned int num_cores = std::thread::hardware_concurrency(); unsigned int num_threads = num_cores; // 使用和核心数一样多的线程
std::cout << "使用 " << num_threads << " 个线程处理图像..." << std::endl;
// 开始计时 auto start_time = std::chrono::high_resolution_clock::now();
// 创建线程并分配工作 std::vector<std::thread> threads; int rows_per_thread = image.size() / num_threads;
for (unsigned int i = 0; i < num_threads; i++) { int start_row = i * rows_per_thread; int end_row = (i == num_threads - 1) ? image.size() : (i + 1) * rows_per_thread;
threads.push_back(std::thread(process_image_part, std::ref(image), start_row, end_row)); }
// 等待所有线程完成 for (auto& t : threads) { t.join(); }
// 结束计时 auto end_time = std::chrono::high_resolution_clock::now(); auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
std::cout << "图像处理完成!耗时: " << duration.count() << " 毫秒" << std::endl;
// 验证结果(只显示部分) std::cout << "处理后的图像样本(左上角): " << std::endl; for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { std::cout << image[i][j] << " "; } std::cout << std::endl; }
return 0;}
复制代码


这个例子展示了如何将大型任务分解成多个小块,分配给多个线程并行处理,充分利用多核 CPU 的优势。


多线程的最佳实践


  1. 保持简单:多线程代码难以调试,尽量简化每个线程的工作。

  2. 避免共享状态:尽可能减少线程间共享的数据,以降低同步复杂度。

  3. 适当的线程数量:通常等于或略多于 CPU 核心数,太多反而会因为频繁切换导致性能下降。

  4. 使用高级抽象:考虑使用std::asyncstd::future或线程池,而不是直接管理线程。

  5. 测试和调试:在各种条件下测试多线程代码,包括高负载和边缘情况。


结语


从此,你已经掌握了 C++11 多线程编程的基础知识!从创建线程到传递参数,从互斥锁到条件变量,从简单示例到实际应用。多线程编程确实比单线程复杂,但掌握了这些技能,你就能写出更高效、响应更快的程序。


记住,多线程编程需要实践和耐心。开始时可能会遇到各种莫名其妙的问题,但随着经验积累,你会越来越熟练。不妨从简单的多线程程序开始,逐步挑战更复杂的场景。


最后的建议:写多线程程序时,时刻保持清醒和警惕,因为多线程 bug 可能是最难调试的 bug 之一!


文章转载自:江小康

原文链接:https://www.cnblogs.com/xiaokang-coding/p/18889729

体验地址:http://www.jnpfsoft.com/?from=001YH

用户头像

还未添加个人签名 2025-04-01 加入

还未添加个人简介

评论

发布
暂无评论
玩转C++11多线程:让你的程序飞起来的std::thread终极指南_C#_电子尖叫食人鱼_InfoQ写作社区