大家好,我是小康。
你还在为 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的优势。
多线程的最佳实践
- 保持简单:多线程代码难以调试,尽量简化每个线程的工作。
- 避免共享状态:尽可能减少线程间共享的数据,以降低同步复杂度。
- 适当的线程数量:通常等于或略多于CPU核心数,太多反而会因为频繁切换导致性能下降。
- 使用高级抽象:考虑使用
std::async
、std::future
或线程池,而不是直接管理线程。 - 测试和调试:在各种条件下测试多线程代码,包括高负载和边缘情况。
结语
从此,你已经掌握了C++11多线程编程的基础知识!从创建线程到传递参数,从互斥锁到条件变量,从简单示例到实际应用。多线程编程确实比单线程复杂,但掌握了这些技能,你就能写出更高效、响应更快的程序。
记住,多线程编程需要实践和耐心。开始时可能会遇到各种莫名其妙的问题,但随着经验积累,你会越来越熟练。不妨从简单的多线程程序开始,逐步挑战更复杂的场景。
最后的建议:写多线程程序时,时刻保持清醒和警惕,因为多线程bug可能是最难调试的bug之一!
愿你的多线程之旅愉快且充满成就感!
🎮 彩蛋时间!
哈喽,开发者朋友们!看到这里,你已经成功将多线程这个"魔法"收入囊中了!是不是感觉自己的代码突然有了开挂的能力?
如果这篇文章帮到了你,不妨点赞、收藏和关注。你的每次互动,都是我熬夜码字的动力源泉啊!⚡
🚀 想继续提升你的C++武器库?
关注我的公众号「跟着小康学编程」,还有更多干货等着你:
- 指针让你头秃?智能指针详解,从此告别段错误噩梦
- STL容器傻傻分不清?一文带你彻底搞懂它们的适用场景
- 设计模式太抽象?用生活案例秒懂,代码立马高大上
- Linux系统编程?从零带你玩转文件、进程、网络编程
每周更新,拒绝空谈,全是能立马用上的实战技巧!
📱 一键关注不迷路
扫码即可关注,让我们一起把 C++ 学得明明白白,用得舒舒服服!
记住:编程难?只是你还没遇到对的老师!跟着小康学,C++没那么难!
💬 加入我们的C++修仙群
遇到 Bug 卡住了?项目思路不清晰?在我们的交流群里,不仅有我这个博主在线答疑,还有一群同样热爱代码的小伙伴随时帮你解惑!