参考:https://llfc.club/articlepage?id=2TayNx5QxbGTaWW5s48vMjtuvCB
1. 简介
主要介绍线程的基本管控,包括线程的发起,等待,异常条件下如何等待以及后台运行等基础操作。
2. 线程的发起
线程发起顾名思义就是启动一个线程,C++11标准统一了线程操作,可以在定义一个线程变量后,该变量启动线程执行回调逻辑。如下即可发起一个线程
在 C++ 中,std::thread
的构造函数用于创建和初始化线程对象。以下是 std::thread
的主要构造函数及其用法示例,结合代码说明如何初始化线程。所有示例都基于 C++11 及以上标准。
std::thread
的构造函数
根据 C++ 标准,std::thread
提供了以下几种构造函数:
-
默认构造函数:
std::thread();
- 创建一个空的线程对象,不关联任何可执行函数,线程未启动。
- 用途:可以稍后通过赋值或移动操作关联线程。
-
带函数和参数的构造函数:
template <class Function, class... Args> explicit std::thread(Function&& f, Args&&... args); // 注意这里的参数为右值
- 创建一个线程并立即启动,执行函数
f
,并将args...
作为参数传递。 Function
可以是函数指针、函数对象、lambda 表达式或可调用对象。- 参数
args
会按值传递(若需传递引用,需用std::ref
)。
- 创建一个线程并立即启动,执行函数
-
移动构造函数(C++11 起):
std::thread(std::thread&& other) noexcept;
- 通过移动语义转移线程对象的所有权,
other
变为空。
- 通过移动语义转移线程对象的所有权,
-
禁用拷贝构造函数:
std::thread(const std::thread&) = delete;
std::thread
不可拷贝,只能移动。
初始化线程的示例
以下是通过不同方式使用构造函数初始化 std::thread
的示例代码:
- 说明:
t5
的所有权通过移动转移到t6
,t5
变为空。
注意事项
-
线程生命周期:
- 线程对象销毁前必须调用
join()
或detach()
,否则程序会调用std::terminate()
终止。 join()
等待线程完成,detach()
让线程在后台运行。
- 线程对象销毁前必须调用
-
参数传递:
- 参数默认按值拷贝到线程的内部存储。若需修改原对象,需用
std::ref
或指针。 - 临时对象(如字符串字面量)会自动转换为
std::string
(如示例 2)。
- 参数默认按值拷贝到线程的内部存储。若需修改原对象,需用
-
异常安全:
- 如果线程启动后抛出异常,确保在
join()
或detach()
前捕获,否则程序可能终止。
- 如果线程启动后抛出异常,确保在
-
性能考虑:
- 线程创建和销毁有开销,考虑使用线程池(如 C++17 的并行算法或第三方库)来管理多线程任务。
使用thread启动一个线程,都会将传入的参数转化为右值存起来
#include <iostream>
#include <thread>
#include <string>
void thread_work1(std::string str)
{
std::cout << "str is " << str << std::endl;
}
int main()
{
std::string hellostr = "Hello!";
// 通过()初始化并启动一个线程
std::thread t1(thread_work1, hellostr);
// 等待线程完成
t1.join();
return 0;
}
3. 线程的等待
当我们启动一个线程后,线程可能没有立即执行,如果在局部作用域启动了一个线程,或者main函数中,很可能子线程没运行就被回收了,回收时会调用线程的析构函数,执行terminate操作。所以为了防止主线程退出或者局部作用域结束导致子线程被析构的情况,我们可以通过join,让主线程等待子线程启动运行,子线程运行结束后主线程再运行。
4. 仿函数作为参数
当我们用仿函数作为参数传递给线程时,也可以达到启动线程执行某种操作的含义
- 歧义
#include <iostream>
#include <thread>
#include <string>
class background_task
{
public:
void operator()(std::string str)
{
std::cout << "str is " << str << std::endl;
}
};
int main()
{
// error 以前歧义:当作函数对象的声明
std::thread t2(background_task());
/*
因为编译器会将t2当成一个函数对象, 返回一个std::thread类型的值,
函数的参数为一个函数指针,该函数指针返回值为background_task,
参数为void。可以理解为如下 类似于:"std::thread (*)(background_task (*)())"
*/
t2.join();
return 0;
}
5. lambda表达式
std::thread t4([](std::string str) {
std::cout << "str is " << str << std::endl;
}, hellostr);
t4.join();
6. 线程detach
允许采用分离的方式在后台独自运行,C++ concurrent programing书中称其为守护线程。
#include <iostream>
#include <thread>
#include <string>
struct func
{
int &_i;
func(int &i) : _i(i) {}
void operator()()
{
for (int i = 0; i < 3; i++)
{
_i = i;
std::cout << "_i is " << _i << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
};
void oops()
{
int some_local_state = 0;
func myfunc(some_local_state);
std::thread functhread(myfunc);
// 隐患,访问局部变量,局部变量可能会随着}结束而回收或随着主线程退出而回收
functhread.detach();
}
int main()
{
// detach 注意事项
oops();
// 防止主线程退出过快,需要停顿一下,让子线程跑起来detach
std::this_thread::sleep_for(std::chrono::seconds(1));
return 0;
}
上面的程序输出不定:有时候是-0,1有时候是0
- detach线程的时候一定要注意,子线程是否用到主线程的局部变量
7. 异常处理
- 当我们启动一个线程后,如果主线程产生崩溃,会导致子线程也会异常退出,就是调用terminate,如果子线程在进行一些重要的操作比如将充值信息入库等,丢失这些信息是很危险的。所以常用的做法是捕获异常,并且在异常情况下保证子线程稳定运行结束后,主线程抛出异常结束运行。如下面的逻辑
#include <iostream>
#include <thread>
#include <string>
struct func
{
int &_i;
func(int &i) : _i(i) {}
void operator()()
{
for (int i = 0; i < 3; i++)
{
_i = i;
std::cout << "_i is " << _i << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
};
void oops()
{
int some_local_state = 0;
func myfunc(some_local_state);
std::thread functhread(myfunc);
// 隐患,访问局部变量,局部变量可能会随着}结束而回收或随着主线程退出而回收
functhread.detach();
}
void catch_exception()
{
int some_local_state = 0;
func myfunc(some_local_state);
std::thread functhread{myfunc};
try
{
// 本线程做一些事情,可能引发崩溃
std::this_thread::sleep_for(std::chrono::seconds(1));
}
catch (std::exception &e)
{
functhread.join();
throw;
}
functhread.join();
}
int main()
{
// detach 注意事项
oops();
// 防止主线程退出过快,需要停顿一下,让子线程跑起来detach
std::this_thread::sleep_for(std::chrono::seconds(1));
return 0;
}
但是用这种方式编码,会显得臃肿,可以采用RAII技术,保证线程对象析构的时候等待线程运行结束,回收资源。如果大家还记得我基于asio实现异步服务时,逻辑处理类LogicSystem的析构函数里等待线程退出。那我们写一个简单的线程守卫
#include <iostream>
#include <thread>
#include <string>
struct func
{
int &_i;
func(int &i) : _i(i) {}
void operator()()
{
for (int i = 0; i < 3; i++)
{
_i = i;
std::cout << "_i is " << _i << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
};
class thread_guard
{
private:
std::thread &_t;
public:
explicit thread_guard(std::thread &t) : _t(t) {}
~thread_guard()
{
// join只能调用一次
if (_t.joinable())
{
_t.join();
}
}
thread_guard(thread_guard const &) = delete;
thread_guard &operator=(thread_guard const &) = delete;
};
void auto_guard()
{
int some_local_state = 0;
func my_func(some_local_state);
std::thread t(my_func);
1 / 0;
thread_guard g(t);
// 本线程做一些事情
std::cout << "auto guard finished " << std::endl;
}
int main()
{
auto_guard();
return 0;
}
8. 慎用隐式转换
C++中会有一些隐式转换,比如char* 转换为string等。这些隐式转换在线程的调用上可能会造成崩溃问题
#include <iostream>
#include <thread>
#include <string>
void print_str(int n, const char* str) {
std::string s(str); // 在线程内部将 const char* 转换为 std::string
std::cout << "print_str: " << s << " (n=" << n << ")" << std::endl;
}
void danger_oops(int som_param) {
char buffer[1024];
sprintf(buffer, "%i", som_param); // 将 som_param 格式化为字符串
std::thread t(print_str, n, buffer); // 创建线程,传递 n 和 buffer
t.detach(); // 分离线程
std::cout << "danger oops finished " << std::endl;
}
当我们定义一个线程变量thread t时,传递给这个线程的参数buffer会被保存到thread的成员变量中。
而在线程对象t内部启动并运行线程时,参数才会被传递给调用函数print_str。
而此时buffer可能随着}运行结束而释放了。
改进的方式很简单,我们将参数传递给thread时显示转换为string就可以了,
这样thread内部保存的是string类型。
#include <iostream>
#include <thread>
#include <string>
void print_str(int n, std::string s) { // 直接接受 std::string
std::cout << "print_str: " << s << " (n=" << n << ")" << std::endl;
}
void safe_oops(int som_param) {
char buffer[1024];
sprintf(buffer, "%i", som_param);
std::thread t(print_str, 3, std::string(buffer)); // 显式转换为 std::string
t.detach();
std::cout << "safe oops finished " << std::endl;
}
int main() {
safe_oops(42);
return 0;
}
9. 引用参数
·当线程要调用的回调函数参数为引用类型时,需要将参数显示转化为引用对象传递给线程的构造函数,
如果采用如下调用会编译失败
#include <iostream>
#include <thread>
#include <string>
void change_param(int ¶m)
{
param++;
}
void ref_oops(int some_param)
{
std::cout << "before change , param is " << some_param << std::endl;
// 需使用引用显示转换
std::thread t2(change_param, some_param);
t2.join();
std::cout << "after change , param is " << some_param << std::endl;
}
int main()
{
ref_oops(2);
return 0;
}
改为如下调用就可以了
void ref_oops(int some_param)
{
std::cout << "before change , param is " << some_param << std::endl;
// 需使用引用显示转换
std::thread t2(change_param, std::ref(some_param));
t2.join();
std::cout << "after change , param is " << some_param << std::endl;
}