学习自 第25课 std::thread对象的析构 - 浅墨浓香 - 博客园 (cnblogs.com)和《c++并发编程实践第二版》2.1
一、线程创建
std::thread th对象创建时,创建线程,这个线程是主线程的子线程,与主线程并发执行
语法
- 每个程序有一个执行 main() 函数的主线程,将函数添加为 std::thread 的参数即可创建另一个线程,两个线程并发运行
#include <iostream>
#include <thread>
void f() { std::cout << "hello world"; }
int main() {
std::thread t{f};
t.join(); // 等待新起的线程退出
}
- std::thread 的参数也可以是函数对象或者 lambda
#include <iostream>
#include <thread>
struct A {//函数对象
void operator()() const { std::cout << 1; }
};
int main() {
/*函数对象*/
A a;
std::thread t1(a); // (对象申明)会调用 A 的拷贝构造函数
std::thread t2(A()); // (函数申明)most vexing parse,声明 名为 t2 、参数类型为 A 的 函数
std::thread t3{A()};
std::thread t4((A()));
/*lambda*/
std::thread t5{[] { std::cout << 1; }};
t1.join();
t3.join();
t4.join();
t5.join();
}
二、td::thread对象的等待与分离
下文中“底层线程”指std::thread对象关联的底层线程。比如创建std::thread th对象时创造的新线程为这个std::thread对象th的底层线程;而在执行th.detach()或th.join()之后,对象th将不与任何底层线程相关联
下文中“线程对象”指类型为std::thread的一个对象
(一)join和detach函数
对线程对象执行join或detach之后,都会和底层线程断开联系,joinable()都会变成返回false。其中执行join之后会释放底层线程的内存(所以这显然要断开线程对象和底层线程的联系);执行detach就是分离底层线程
### 1. 线程等待/联结:join()
(1)等待子线程结束,调用线程处于阻塞模式。
(2)join()执行完成之后,底层线程id被设置为0,即joinable()变为false。同时会清理线程相关的存储部分, 这样 std::thread 对象将不再与底层线程有任何关联。这意味着,**只能对一个线程对象使用一次join()**。
### 2. 线程分离:detach()
(1)分离子线程,与当前线程的连接被断开,子线程成为后台线程,被C++运行时库接管。 std::thread 对象将不再与底层线程有任何关联。与join一样,detach也只能调用一次,当detach以后其joinable()为false。
(2)注意事项:
①如果不等待线程,就必须保证线程结束之前,可访问的数据是有效的。**特别是要注意线程函数是否还持有一些局部变量的指针或引用(这些局部变量所在作用域在线程函数返回前可能已经结束了)**。
悬空引用问题:
#include <iostream>
#include <thread>
using namespace std;
class FuncObject
{
void do_something(int& i) { cout <<"do something: " << i << endl; }
public:
int& i; //注意:引用类型
FuncObject(int& i) :i(i) { } //注意:引用类型
void operator()()
{
for (unsigned int j = 0; j < 1000; ++j)
{
do_something(i); //可能出现悬空引用的问题。
}
}
};
void oops()
{
int localVar = 0;
FuncObject fObj(localVar); //注意,fObj的成员i是引用局部变量localVar
std::thread t1(fObj);
t1.detach(); //主线程调用oops函数,可能出现oops函数执行完了,子线程还在运行的现象。它会去调用do_something,这时会访问到己经被释放的localVar变量,会出现未定义行为!如果这里改成join()则不会发生这种现象。因为主线程会等子线程执行完才退出oops
}
int main()
{
oops();
return 0;
}
②为防止上述的悬空指针和悬引用的问题,线程对象(std::thread对象)的生命期应尽量长于底层线程(std::thread对象创造时创造的线程)的生命期,或者将局部变量复制传入线程,而不是引用或指针
(3)应用场合
①适合长时间运行的任务,如后台监视文件系统、对缓存进行清理、对数据结构进行优化等。
②线程被用于“发送即不管”(fire and forget)的任务,任务完成情况线程并不关心,即安排好任务之后就不管。
(二)联结状态:
线程对象的联结状态即std::thread对象是否与某个有效的底层线程关联,可用joinable()函数来判断,内部通过判断线程id是否为0来实现。一个std::thread对象只可能处于可联结或不可联结两种状态之一。
1. **不可联结**:
1. **己调用join或detach的std::thread对象**为不可联结状态。
2. **不带参构造的std::thread对象**为不可联结,因为底层线程还没创建。
3. **己移动的std::thread对象**为不可联结。因为该对象的底层线程id会被设置为0。
- 可联结:
当线程可运行、己运行或处于阻塞时是可联结的std::thread对象生命周期内的其他情况。注意,如果某个底层线程已经执行完任务,但是没有被join的话,该线程依然会被认为是一个活动的执行线程,该std::thread对象仍然处于joinable状态。
三、std::thread对象的析构
(一)std::thread的析构
- std::thread对象析构时,会先判断joinable(),如果可联结,则程序会直接被终止(std::thread对象的析构函数中会调用``std::terminate`函数)。
std::terminate
调用当前安装的std::terminate_handler
。默认的std::terminate_handler
调用std::abort
这好像会终止进程
- 这意味std::thread对象从其它定义域出去的任何路径,都应为不可联结状态。也意味着创建thread对象以后,要在随后的某个地方显式地调用join或detach以便让std::thread处于不可联结状态。
(二)为什么析构函数中不隐式调用join或detach?
如果设计成隐式join():将导致调用线程一直等到子线程结束才返回。如果子线程正在运行一个耗时任务,这可能造成性能低下的问题,而且问题也不容易被发现。
如果设计成隐式detach():由于detach会将切断std::thread对象与底层线程之间的关联,两个线程从此各自独立运行。如果线程函数是按引用(或指针)方式捕捉的变量,在调用线程退出作用域后这些变量会变为无效,这容易掩盖错误也将使调试更加困难。因此隐式detach,还不如join或者显式调用detach更直观和安全。
标准委员会认为,销毁一个joinable线程的后果是十分可怕的,因此他们通过terminate程序来禁止这种行为。为了避免销毁一个joinable的线程,就得由程序员自己来确保std::thread对象从其定义的作用域出去的任何路径,都处于不可联结状态,最常用的方法就是资源获取即初始化技术(RAII,Resource Acquisition Is Initialization)。
(三)利用RAII技术:保证从std::thread对象定义的作用域出去的任何路径,都处于不可联结状态
异常抛出情况下
如果主线程(创造std::thread对象的线程)运行后在执行join()之前抛出异常,这样就会跳过join(),即这种情况下主线程可能会比它join的线程先结束,因为它中止在异常抛出的处理中了,而没有走到join,这种情况下主线程在结束的时候析构std::thread对象时,该对象是joinable的,则整个进程会终止
因此,如果在无异常的情况下要使用join(),则需要**在异常处理中也调用join()**,从而避免生命周期的问题。
struct func
{
int& i; //注意这个是引用
func(int& i_) : i(i_) {} //构造函数的参数是引用类型
void operator() ()
{
for (unsigned j=0 ; j<1000000 ; ++j)
{
do_something(i); // 1 潜在访问隐患:空引用
}
}
};
void f()
{
int some_local_state=0;
func my_func(some_local_state);
std::thread t(my_func);
try
{
do_something_in_current_thread();
}
catch(...)
{
t.join(); // 1
throw;
}
t.join(); // 2
}
代码2.2中使用了try/catch
块确保(子)线程退出后(主线程的f)函数才结束。当函数正常退出后,会执行到②处。当执行过程中抛出异常,程序会执行到①处。如果线程在函数之前结束——就要查看是否因为线程函数使用了局部变量的引用——而后再确定一下程序可能会退出的途径,无论正常与否,有一个简单的机制,可以解决这个问题。
RAII方案
1. 方案1:自定义的thread_guard类,并将std::thread对象传入其中,同时在构造时选择join或detach策略。当thread_guard对象析构时,会根据析构策略,**调用std::thread的join()或detach()**,确保在任何路径,线程对象都处于unjoinable状态。
2. 方案2:重新封装std::thread类(见下面的代码,类名为joining_thread),在**析构时隐式调用join()**。
#include <iostream>
#include <thread>
class thread_guard
{
std::thread t;
public:
//构造函数
explicit thread_guard(std::thread t_):
t(std::move(t_))
{}
//析构函数
~thread_guard()
{
if(t.joinable())
{
t.join();
}
}
//拷贝语义函数删除
thread_guard(thread_guard const&)=delete;
thread_guard& operator=(thread_guard const&)=delete;
};
struct func;
void f()
{
int some_local_state=0;
func my_func(some_local_state);
std::thread t(my_func);//创建新线程
thread_guard g(std::move(t)); //调用thread_guard的构造函数,其中构造参数_t(_t是类型为std::thread的bian'lian)会调用std::thread的移动构造函数(把底层线程所有权给_t),然后t(std::move(_t))也会调用std::thread的移动构造函数(把底层线程所有权给t)
do_something_in_current_thread();
//主线程离开这里时,析构thread_guard g对象,调用其析构函数
}
代码尚未阅读
//使用RAII等待线程完成:joining_thread类的实现
class joining_thread
{
std::thread thr;
public:
joining_thread() noexcept = default;
//析构函数
~joining_thread()
{
if (joinable()) //对象析构造,会隐式调用join()
{
join();
}
}
template<typename Callable, typename... Args>
explicit joining_thread(Callable&& func, Args&& ...args):
thr(std::forward<Callable>(func), std::forward<Args>(args)...)
{
}
//类型转换构造函数
explicit joining_thread(std::thread t) noexcept : thr(std::move(t))
{
}
//移动操作
joining_thread(joining_thread&& other) noexcept : thr(std::move(other.thr))
{
}
joining_thread& operator=(joining_thread&& other) noexcept
{
if (joinable()) join(); //等待原线程执行完
thr = std::move(other.thr); //将新线程移动到thr中
return *this;
}
joining_thread& operator=(std::thread other) noexcept
{
if (joinable()) join();
thr = std::move(other);
return *this;
}
bool joinable() const noexcept
{
return thr.joinable();
}
void join() { thr.join(); }
void detach() { thr.detach(); }
void swap(joining_thread& other) noexcept { thr.swap(other.thr); }
std::thread::id get_id() const noexcept { return thr.get_id(); }
std::thread& asThread() noexcept //转化为std::thread对象
{
return thr;
}
const std::thread& asThread() const noexcept
{
return thr;
}
};
void doWork(int i) { cout << i << endl; }
int main()
{
//3. 测试joining_thread类
std::vector<joining_thread> threads; //joining_thread析构时隐式调用join
for (unsigned int i = 0; i < 20; ++i) {
threads.push_back(joining_thread(doWork, i));
}
std::for_each(threads.begin(), threads.end(), std::mem_fn(&joining_thread::join));
return 0;
}
四、std::thread的移动语义
创建了两个执行线程,并在std::thread
实例之间(t1,t2和t3)转移所有权:
void some_function();
void some_other_function();
std::thread t1(some_function); // 1
std::thread t2=std::move(t1); // 2 std::move返回的右值类型触发调用 std::thread的移动赋值重载函数,这个移动语义函数实现底层线程的“所有权”从t1移动到t2
//此时t1无关联线程
t1=std::thread(some_other_function); // 3 临时std::thread对象启动了一个线程,临时对象是一个右值,因此触发调用 std::thread的移动赋值重载函数,将这个新触发的线程移动给t1
//现在t1和刚刚std::thread临时对象启动的线程相关联
std::thread t3; // 4 t3此时没有与任何线程进行关联
t3=std::move(t2); // 5
t1=std::move(t3); // 6 因为t1已经有了一个关联的线程,赋值操作将抛出异常,移动赋值重载函数是noexcept则异常处理机制将直接调用std::terminate()
函数返回std::thread
对象:
std::thread f()
{
void some_function();
return std::thread(some_function); //构造返回值std::thread临时对象时是调用std::thread的移动构造函数
}
//这个编译器不会报错吗?试了一下不会
std::thread g()
{
void some_other_function(int);
std::thread t(some_other_function,42);
return t; //构造返回值std::thread临时对象时是调用std::thread的移动构造函数
//return std::move(t);比较符合我的li
}
std::thread
实例作为参数进行传递:
void f(std::thread t);
void g()
{
void some_function();
f(std::thread(some_function));
std::thread t(some_function);
f(std::move(t));
}