线程发起
最简单的发起一个线程。
cpp
void thread_work(std::string str) {
std::cout << "str: " << std << std::endl;
}
//初始化并启动一个线程
std::thread t1(thread, wangzn2016);
线程等待:
线程发起后,可能新的线程还没立即执行,或者还没执行完成,主线程就执行完了,此时子线程就会被回收,回收时会调用线程的析构函数,执行terminate操作。
因此,为了防止主线程退出或者局部作用域结束导致子线程被回收析构,我们可以通过join的方式,让主线程等待子线程执行完成并回收。
cpp
void thread_work(std::string str) {
std::cout << "str: " << std << std::endl;
}
//初始化并启动一个线程
std::thread t1(thread, wangzn2016);
//主线程等待子线程执行完成
t1.join();
仿函数作为参数
也可以使用仿函数作为参数发起线程
cpp
class background_task {
public:
void operator()() {
std::cout << "wangzn2016" << str << std::endl;
}
};
//这种方式会被编译器识别成函数"std::stread (*)(background_task) (*)()"
//std::thread t1(background_task());
//其中一个解决办法就是,实例化一个对象后,再传入
//这里提供另外两种种解决办法
// 1.多加一层()
std::thread t2((background_task()));
t2.join();
// 2.使用{}来初始化
std::thread t3{ background_task() };
t3.join();
Lambda表达式
lambda表达式也可以作为线程的参数传递给thread。
cpp
std::thread t1([](std::string str) {
std::cout << "str: " << str << std::endl;
}, hellostr);
t1.join();
线程detach
线程分离,也可以被称为守护线程。线程允许采用分离的方式在后台独自运行。
cpp
struct func {
int& _i;
func(int& i): _i(i){}
void operator()() {
for (int i = 0; i < 3; i++) {
_i = i;
std::cout << "_i: " << _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();
}
// detach 注意事项
oops();
//防止主线程退出过快,需要停顿一下,让子线程跑起来detach
std::this_thread::sleep_for(std::chrono::seconds(1));
上面的例子存在隐患,因为some_local_state
是局部变量, 当oops
调用结束后局部变量some_local_state
就可能被释放了,而线程还在detach后台运行,容易出现崩溃。
解决办法:
- 通过智能指针传递参数,因为引用计数会随着赋值增加,可保证局部变量在使用期间不被释放,这也就是我们之前提到的伪闭包策略。
- 将局部变量的值作为参数传递,这么做需要局部变量有拷贝复制的功能,而且拷贝耗费空间和效率。
- 将线程运行的方式修改为join,这样能保证局部变量被释放前线程已经运行结束。但是这么做可能会影响运行逻辑。 比如下面的修改
cpp
void use_join()
{
int some_local_state = 0;
func myfunc(some_local_state);
std::thread functhread(myfunc);
functhread.join();
}
异常处理
cpp
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();
}
但是用这种方式编码,会显得臃肿,可以采用RAII技术,保证线程对象析构的时候等待线程运行结束,回收资源。
cpp
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);
thread_guard g(t);
//本线程做一些事情
std::cout << "auto guard finished " << std::endl;
}
auto_guard();
慎用隐式类型转换
c++中会有一些隐式类型转换,比如char*转换为string等。这些隐式类型转换在线程的调用上可能会造成崩溃问题
cpp
void print_str(int i, std::string str)
{
std::cout << "i: " << i << " std: " << str << std::endl;
}
void danger_oops(int som_param) {
char buffer[1024];
sprintf(buffer, "%i", som_param);
//在线程内部将char const* 转化为std::string
std::thread t(print_str, 3, buffer);
t.detach();
std::cout << "danger oops finished " << std::endl;
}
我们定义一个线程变量thread t
时,传递给这个线程的参数buffer
会被保存到thread
的成员变量中。 而在线程对象t内部启动并运行线程时,参数才会被传递给调用函数print_str
。 而此时buffer
可能随着}
运行结束而释放了。
解决的方式很简单,我们将参数传递给thread
时显示转换为string
就可以了, 这样thread
内部保存的是string
类型。
cpp
void safe_oops(int some_param) {
char buffer[1024];
sprintf(buffer, "%i", some_param);
std::thread t(print_str, 3, std::string(buffer));
t.detach();
}
引用参数
当我们创建的线程要调用的回调函数的参数为引用类型的时候,需要将参数显示转化为引用对象传递给线程的构造函数。
cpp
void change_param(int& param) {
param++;
}
void ref_oops(int some_param) {
std::cout << "before change , param is " << some_param << std::endl;
//需使用引用显示转换, std::ref
std::thread t2(change_param, std::ref(some_param));
t2.join();
std::cout << "after change , param is " << some_param << std::endl;
}
因为线程的启动在底层是模板实现,并且嵌套多个函数,引用类型在传参的时候涉及到引用降级(引用折叠),最后传递给change_param函数的参数是个右值,因此会报错,所以我们需要使用引用显示转换, std::ref()来传参。
thread原理
cpp
template <class _Fn, class... _Args, enable_if_t<!is_same_v<_Remove_cvref_t<_Fn>, thread>, int> = 0>
_NODISCARD_CTOR explicit thread(_Fn&& _Fx, _Args&&... _Ax) {
_Start(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
}
thread构造函数内部通过forward
原样转换传递给_Start
函数。关于原样转换的知识可以看我之前写的文章。 _Start
函数内部就是启动了一个线程_beginthreadex
执行回调函数。
cpp
template <class _Fn, class... _Args>
void _Start(_Fn&& _Fx, _Args&&... _Ax) {
using _Tuple = tuple<decay_t<_Fn>, decay_t<_Args>...>;
auto _Decay_copied = _STD make_unique<_Tuple>(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
constexpr auto _Invoker_proc = _Get_invoke<_Tuple>(make_index_sequence<1 + sizeof...(_Args)>{});
#pragma warning(push)
#pragma warning(disable : 5039) // pointer or reference to potentially throwing function passed to
// extern C function under -EHc. Undefined behavior may occur
// if this function throws an exception. (/Wall)
_Thr._Hnd =
reinterpret_cast<void*>(_CSTD _beginthreadex(nullptr, 0, _Invoker_proc, _Decay_copied.get(), 0, &_Thr._Id));
#pragma warning(pop)
if (_Thr._Hnd) { // ownership transferred to the thread
(void) _Decay_copied.release();
} else { // failed to start thread
_Thr._Id = 0;
_Throw_Cpp_error(_RESOURCE_UNAVAILABLE_TRY_AGAIN);
}
}