大家好,我是小康。
你还在为 C++ 多线程编程发愁吗?别担心,今天咱们就用大白话彻底搞定std::thread!看完这篇,保证你对C++11多线程的理解从"一脸懵逼"变成"原来如此"!
前言:为啥要学多线程?
想象一下,你正在厨房做饭。如果你是单线程工作,那就只能先切菜,切完再炒菜,炒完再煮汤...一项一项按顺序来。但现实中的你肯定是多线程操作啊:锅里炒着菜,同时旁边的电饭煲在煮饭,热水壶在烧水,也许你还能同时看看手机...这就是多线程的威力!
在程序世界里,多线程就像多了几个"分身",可以同时处理不同的任务,充分利用多核CPU的性能,让程序跑得飞快。特别是现在谁的电脑不是多核啊,不用多线程简直是浪费资源!
C++11标准终于给我们带来了官方的多线程支持------std::thread,从此不用再依赖操作系统特定的API或第三方库,写多线程程序方便多了!
微信搜索 「跟着小康学编程」,关注我,后续还有更多硬核技术文章分享,带你玩转 Linux C/C++ 编程!😆
第一步:创建你的第一个线程
好,闲话少说,直接上代码看看怎么创建一个线程:
cpp
#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;
}
输出结果可能是:
plain
主线程:我正在等一个线程干活...
哈喽,我是一个新线程!
所有线程都结束了,程序退出!
或者是:
plain
哈喽,我是一个新线程!
主线程:我正在等一个线程干活...
所有线程都结束了,程序退出!
咦?为啥输出顺序不固定?因为两个线程是并发执行的,谁先打印完全看 CPU 的心情!这就是多线程的特点------不确定性。
代码解析:
创建线程超简单,就一行代码:std::thread t(hello_thread);
。线程一创建就立刻开始执行了,就像放出去的炮弹,发射了就收不回来了。
t.join()
是啥意思呢?它相当于说:"主线程,你等等这个新线程,等它干完活再继续"。如果没有这行,主线程可能提前结束,程序就崩溃了!
给线程传参数
线程不能只会喊"哈喽"吧?我们得给它点实际任务,还得告诉它一些参数。传参数超简单:
cpp
#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;
}
输出结果:
plain
主线程:我让线程去问候张三了...
你好,张三!这是第 1 次问候!
你好,张三!这是第 2 次问候!
你好,张三!这是第 3 次问候!
问候完毕!
传参就像普通函数调用一样,直接在线程构造函数后面加参数就行。但是有个坑:参数是"拷贝"到线程中的,所以小心对象的复制开销!
用Lambda表达式创建线程
每次都要单独写个函数太麻烦了,有没有简单方法?有啊,用Lambda表达式!
cpp
#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表达式就像一个临时小函数,用完就扔,方便得很!特别适合那种只用一次的简单逻辑。
多线程通信的坑:数据竞争
多线程编程最大的坑就是多个线程同时访问同一数据时会出现"数据竞争"。来看个例子:
cpp
#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;
}
输出可能是:
plain
理论上counter应该等于:50000
实际上counter等于:42568
咦?怎么少了那么多?因为 counter++
看起来是一条语句,但实际上分三步:读取counter的值、加1、写回counter。当多个线程同时执行这个操作,就会互相"踩踏",导致最终结果小于预期。
这就是臭名昭著的数据竞争问题,解决方法有互斥锁、原子操作等,后面会讲。
微信搜索 「跟着小康学编程」,关注我,后续还有更多硬核技术文章分享,带你玩转 Linux C/C++ 编程!😆
线程管理的基本操作
1. join() - 等待线程完成
我们已经见过 join()
了,它会阻塞当前线程,直到目标线程执行完毕。
cpp
std::thread t(some_function);
t.join(); // 等待t完成
2. detach() - 让线程"自生自灭"
有时候,我们启动一个线程后不想等它了,可以用 detach()
让它独立运行:
cpp
std::thread t(background_task);
t.detach(); // 线程在后台独立运行
std::cout << "主线程不管子线程了,继续自己的事" << std::endl;
detach后的线程称为"分离线程"或"守护线程",它会在后台默默运行,直到自己的任务完成。但要小心:如果主程序结束了,这些分离线程会被强制终止!
3. joinable() - 检查线程是否可等待
在join之前,最好检查一下线程是否可以被等待:
cpp
std::thread t(some_function);
// ... 一些代码 ...
if (t.joinable()) {
t.join();
}
这避免了对已经 join 或 detach 过的线程再次操作,否则会崩溃。
防止忘记join:RAII风格的线程包装器
C++的经典模式:用对象的生命周期管理资源。我们可以创建一个线程包装器,在析构时自动join:
cpp
#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):
cpp
#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;
}
输出:
plain
现在counter正确等于:50000
太好了!结果正确了。但这样手动lock/unlock很容易出错,如果忘记unlock或者发生异常,就会死锁。所以更推荐使用RAII风格的std::lock_guard
:
cpp
void better_safe_increment(int times) {
for (int i = 0; i < times; i++) {
std::lock_guard<std::mutex> lock(counter_mutex); // 自动锁定和解锁
counter++;
}
}
lock_guard
在构造时锁定互斥锁,在析构时自动解锁,无论是正常退出还是异常退出都能保证互斥锁被释放。
高级话题:条件变量
线程间的同步不只有互斥,有时我们需要一个线程等待某个条件满足。条件变量就是干这个的:
cpp
#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;
}
这个例子展示了经典的"生产者-消费者"模式:生产者往队列里放数据,消费者从队列里取数据。条件变量确保消费者不会在队列为空时尝试取数据。
线程与异常安全
在多线程程序中处理异常尤为重要。如果线程执行时抛出异常,且没被捕获,整个程序会直接崩溃!以下是安全处理方式:
cpp
#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;
}
输出:
plain
线程捕获到异常: 故意抛出的异常!
程序正常结束
记住:每个线程都有自己独立的调用栈,异常不会跨线程传播!在哪个线程抛出,就必须在哪个线程捕获。
实用技巧
1. 获取线程ID
每个线程都有唯一的ID,用于标识:
cpp
#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. 线程休眠
有时需要让线程暂停一会儿:
cpp
#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核心优化线程数量:
cpp
#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;
}
实际案例:并行图像处理
来个实际应用案例:用多线程加速图像处理。这里我们简化为操作一个二维数组:
cpp
#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 卡住了?项目思路不清晰?在我们的交流群里,不仅有我这个博主在线答疑,还有一群同样热爱代码的小伙伴随时帮你解惑!