首页/文章列表/文章详情

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

编程知识782025-05-21评论

大家好,我是小康。

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

前言:为啥要学多线程?

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

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

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

微信搜索 「跟着小康学编程」,关注我,后续还有更多硬核技术文章分享,带你玩转 Linux C/C++ 编程!😆

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

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

#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。当多个线程同时执行这个操作,就会互相"踩踏",导致最终结果小于预期。

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

微信搜索 「跟着小康学编程」,关注我,后续还有更多硬核技术文章分享,带你玩转 Linux C/C++ 编程!😆

线程管理的基本操作

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之一!

愿你的多线程之旅愉快且充满成就感!


🎮 彩蛋时间!

哈喽,开发者朋友们!看到这里,你已经成功将多线程这个"魔法"收入囊中了!是不是感觉自己的代码突然有了开挂的能力?

如果这篇文章帮到了你,不妨点赞、收藏和关注。你的每次互动,都是我熬夜码字的动力源泉啊!⚡

🚀 想继续提升你的C++武器库?

关注我的公众号「跟着小康学编程」,还有更多干货等着你:

  • 指针让你头秃?智能指针详解,从此告别段错误噩梦
  • STL容器傻傻分不清?一文带你彻底搞懂它们的适用场景
  • 设计模式太抽象?用生活案例秒懂,代码立马高大上
  • Linux系统编程?从零带你玩转文件、进程、网络编程

每周更新,拒绝空谈,全是能立马用上的实战技巧!

📱 一键关注不迷路

扫码即可关注,让我们一起把 C++ 学得明明白白,用得舒舒服服!

记住:编程难?只是你还没遇到对的老师!跟着小康学,C++没那么难!

💬 加入我们的C++修仙群

遇到 Bug 卡住了?项目思路不清晰?在我们的交流群里,不仅有我这个博主在线答疑,还有一群同样热爱代码的小伙伴随时帮你解惑!

神弓

江小康

这个人很懒...

用户评论 (0)

发表评论

captcha