引言
在C++11标准之前,并没有引入线程这个概念,如果我们想要在C++中实现多线程,需要借助操作系统平台提供的API,例如在Linux中的<thread>头文件,它解决了跨平台的问题,提供了管理线程、保护共享数据、线程间同步操作、原子操作等类。本节我们先来了解线程创建部分。
创建线程
创建线程的关键是传递一个callable的对象。
普通函数无参
#include <iostream>
#include <thread>
using namespace std;
void basicFunc() {
cout << "Hello World!" << endl;
}
int main() {
thread t(basicFunc);
t.join();
return 0;
}
输出:
Hello World!
普通函数 + 参数
#include <iostream>
#include <thread>
using namespace std;
void printMsg(int x, const string& s) {
cout << "x = " << x << " s = " << s << endl;
}
int main() {
thread t(printMsg, 100, move(string("Hello World!")));
t.join();
return 0;
}
输出:
x = 100 s = Hello World!
类非静态成员函数
#include <iostream>
#include <thread>
using namespace std;
class Task {
public:
void work(int hours) {
cout << "works for " << hours << " hours" << endl;
}
};
int main() {
Task task;
// 构造线程: 成员函数指针 + 对象指针 + 参数列表
thread t(&Task::work, &task, 8);
t.join();
return 0;
}
输出:
works for 8 hours
类静态成员函数
#include <iostream>
#include <thread>
using namespace std;
class Util {
public:
static void staticFunc() { cout << "静态成员函数线程" << endl; }
};
int main() {
thread t1(Util::staticFunc); // 静态成员函数
t1.join();
return 0;
}
输出:
静态成员函数线程
函数对象(仿函数)
#include <iostream>
#include <thread>
using namespace std;
class Counter {
private:
int count;
public:
Counter(int init) : count(init) {}
// 重载()操作符
void operator()(int step) {
for (int i = 0; i < 3; ++i) {
count += step;
cout << "当前计数:" << count << endl;
}
}
int getCount() {
return count;
}
};
int main() {
Counter counter(10);
cout << "before thread: " << counter.getCount() << endl;
thread t(counter, 5); // 函数对象作为任务(会拷贝对象)
t.join();
cout << "after thread: " << counter.getCount() << endl;
return 0;
}
输出:
before thread: 10
当前计数:15
当前计数:20
当前计数:25
after thread: 10
lambda表达式
#include <iostream>
#include <thread>
using namespace std;
int main() {
int num = 100;
string msg = "Lambda线程";
// 捕获外部变量:值捕获msg,引用捕获num
thread t([msg, &num]() {
num += 200;
cout << "msg=" << msg << ", num=" << num << endl;
});
t.join();
cout << "主线程num:" << num << endl; // 输出300(被线程修改)
return 0;
}
输出:
msg=Lambda线程, num=300
主线程num:300
参数传递与移动语义
在上文的创建线程中,对线程的参数传递做了几个示例,这里再做一下总结。
普通参数传递
值传递:直接传值会将参数拷贝到线程栈空间中;
引用传递:使用std::ref或者std::cref包装,可以将主线程中的变量按引用方式传递到线程中
移动语义
对于大对象场景,例如string、vector,不可拷贝对象unique_ptr, thread可通过std::move将对象转移到线程内部。
这里以string为例举例说明:
#include <iostream>
#include <thread>
#include <vector>
#include <string>
#include <utility>
using namespace std;
void processString(string&& s) {
s = "处理后:" + s;
cout << s << endl;
}
int main() {
vector<string> task_list = {"任务1", "任务2", "任务3"};
vector<thread> threads;
for (auto& s : task_list) {
// 移动传递string,避免拷贝大对象
threads.emplace_back(processString, move(s));
}
for (auto& t : threads) t.join();
for (int i = 0; i < task_list.size(); i++) {
if (task_list[i].empty()) {
cout << "task " << i << " empty" << endl;
}
}
return 0;
}
输出:
处理后:任务1
处理后:任务2
处理后:任务3
task 0 empty
task 1 empty
task 2 empty
这里模拟大任务场景下,通过移动语义避免copy开销。
async/packaged_task方式创建线程
为了更方便地使用thread,C++11中还引入了async和packaged_task两种方式创建线程。可以认为是高层并发组件。可以认为是thread的封装和扩展(扩展了结果传递、任务绑定、异常转发等能力)。
这是因为thread作为底层线程工具,存在两个核心的缺陷,导致日常开发效率低、易出错:
- 缺陷 1:无法直接获取任务返回值:
std::thread的线程函数返回值会被忽略,需手动用「共享变量 + 互斥锁」传递结果(繁琐且易引发线程安全问题); - 缺陷 2:无法传递线程内异常:线程函数抛出的未捕获异常会直接导致程序崩溃,需手动在线程内捕获并传递到主线程(代码冗余);
- 缺陷 3:任务与线程强耦合:
std::thread直接绑定执行流,无法单独存储 “任务”(如线程池场景需要先存储任务再分配线程)。
std::packaged_task 和 std::async 的核心作用,就是通过封装解决这些缺陷,让开发者聚焦 “任务逻辑” 而非 “线程管理细节”。
std::packaged_task
std::packaged_task 是一个任务包装器,核心功能是:
- 包装任意可调用对象(函数、Lambda、函数对象等);
- 将任务的返回值与一个
std::future对象绑定; - 任务执行后,返回值会自动存入
future,主线程通过future.get()获取(支持同步等待)。
需要注意 CLion中的默认编译器是精简版,不支持packaged_task
#include <iostream>
#include <thread>
#include <packaged_task>
#include <future>
#include <string>
using namespace std;
// 任务:处理字符串并返回
string process(string s) {
return "[处理后] " + s;
}
int main() {
// 1. 包装任务:绑定任务函数和结果类型(string(string))
packaged_task<string(string)> task(process);
// 2. 获取与任务绑定的 future(用于接收返回值)
future<string> fut = task.get_future();
// 3. 手动创建线程执行任务(必须用 move 转移 task,因为它不可拷贝)
thread t(move(task), "测试任务");
t.join(); // 等待线程完成
// 4. 获取任务返回值(get() 会阻塞,直到任务完成)
string res = fut.get();
cout << res << endl; // 输出:[处理后] 测试任务
return 0;
}
std::async
std::async 是一个异步任务创建函数,核心功能是:
- 自动包装任务(无需手动创建
packaged_task); - 自动创建线程(或复用线程池)执行任务;
- 自动返回
std::future对象,供主线程获取结果; - 无需手动管理
std::thread的join()/detach()(内部自动处理线程生命周期)。
#include <iostream>
#include <future>
#include <string>
using namespace std;
string process(string s) {
return "[处理后] " + s;
}
int main() {
// 1. 自动创建异步任务,返回 future(无需手动管理 thread)
future<string> fut = async(process, "测试任务");
// 主线程可并行执行其他逻辑
cout << "主线程执行其他工作..." << endl;
// 2. 获取结果(若任务未完成,get() 会阻塞)
string res = fut.get();
cout << res << endl; // 输出:[处理后] 测试任务
return 0;
}
线程异常处理
线程函数中未捕获的异常会导致 std::terminate() 调用,程序直接崩溃(无论是否 join 或 detach)。
举例:
#include <iostream>
#include <thread>
#include <stdexcept>
using namespace std;
void throwFunc() {
throw runtime_error("线程内异常"); // 未捕获异常
}
int main() {
thread t(throwFunc);
t.join(); // 程序依然崩溃
return 0;
}
输出:
terminate called after throwing an instance of 'std::runtime_error'
what(): 线程内异常
对于这种情况有两种处理方式:
方式一:线程内部处理
#include <iostream>
#include <thread>
#include <stdexcept>
using namespace std;
void safeFunc() {
try {
throw runtime_error("线程内异常");
} catch (const exception& e) {
// 线程内处理异常
cout << "线程内捕获异常:" << e.what() << endl;
}
}
int main() {
thread t(safeFunc);
t.join();
cout << "主线程正常执行" << endl; // 会执行
return 0;
}
方式 2:通过 std::future 传递异常到主线程(推荐)
std::async/std::packaged_task 会自动捕获线程内的异常,存储到 future 中,主线程调用 get() 时会重新抛出异常,便于统一处理。
#include <iostream>
#include <future>
#include <stdexcept>
#include <string>
using namespace std;
string throwFunc() {
// 线程内抛出异常
throw runtime_error("线程处理失败:数据格式错误");
return "处理成功"; // 永远不会执行
}
int main() {
future<string> fut = async(throwFunc);
try {
string res = fut.get(); // get() 会重新抛出线程内的异常
cout << res << endl;
} catch (const exception& e) {
// 主线程统一捕获处理
cout << "主线程捕获线程异常:" << e.what() << endl;
}
cout << "主线程正常继续" << endl; // 会执行
return 0;
}
主线程捕获线程异常:线程处理失败:数据格式错误
主线程正常继续
主子线程交互
join方法
主线程阻塞等待子线程完成,回收线程资源。
detatch方法
子线程与主线程分离,后台独立运行,由系统自动回收资源(需确保子线程访问的变量生命周期足够长)。
总结
本小节详细介绍了C++线程的创建相关内容,在使用时需要基于场景灵活选择创建方式,并注意参数传递时移动语义的使用。