一. 线程的等待与分离ios
(一)join和detach函数程序员
1. 线程等待:join()编程
(1)等待子线程结束,调用线程处于阻塞模式。缓存
(2)join()执行完成以后,底层线程id被设置为0,即joinable()变为false。同时会清理线程相关的存储部分, 这样 std::thread 对象将再也不与已经底层线程有任何关联。这意味着,只能对一个线程使用一次join();调用join()后,joinable()返回false。安全
2. 线程分离:detach()数据结构
(1)分离子线程,与当前线程的链接被断开,子线程成为后台线程,被C++运行时库接管。这意味着不可能再有std::thread对象能引用到子线程了。与join同样,detach也只能调用一次,当detach之后其joinable()为false。函数
(2)注意事项:oop
①若是不等待线程,就必须保证线程结束以前,可访问的数据是有效的。特别是要注意线程函数是否还持有一些局部变量的指针或引用。性能
②为防止上述的悬空指针和悬引用的问题,线程对象的生命期应尽可能长于底层线程的生命期。测试
(3)应用场合
①适合长时间运行的任务,如后台监视文件系统、对缓存进行清理、对数据结构进行优化等。
②线程被用于“发送即无论”(fire and forget)的任务,任务完成状况线程并不关心,即安排好任务以后就无论。
(二)联结状态:一个std::thread对象只可能处于可联结或不可联结两种状态之一。可用joinable()函数来判断,即std::thread对象是否与某个有效的底层线程关联(内部经过判断线程id是否为0来实现)。
1. 可联结(joinable):当线程可运行、己运行或处于阻塞时是可联结的。注意,若是某个底层线程已经执行完任务,可是没有被join的话,该线程依然会被认为是一个活动的执行线程,仍然处于joinable状态。
2. 不可联结(unjoinable):
(1)当不带参构造的std::thread对象为不可联结,由于底层线程还没建立。
(2)己移动的std::thread对象为不可联结。由于该对象的底层线程id会被设置为0。
(3)己调用join或detach的对象为不可联结状态。由于调用join()之后,底层线程己结束,而detach()会把std::thread对象和对应的底层线程之间的链接断开。
【编程对象】等待与分离
#include <iostream> #include <thread> using namespace std; //1. 悬空引用问题 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); std::thread t1(fObj); t1.detach(); //子线程分离,转为后台运行。主线程调用oops函数,可能出现oops函数 //执行完了,子线程还在运行的现象。它会去调用do_something,这时会 //访问到己经被释放的localVar变量,会出现未定义行为!若是这里改为 //join()则不会发生这种现象。所以主线程会等子线程执行完才退出oops } //2. 利用分离线程处理多文档文件 void openDocAndDisplay(const std::string& fileName){} //打开文件 bool doneEditing() { return false; } //判断是否结束编辑 enum class UserCommand{OpenNewDocument, SaveDocument,EditDocument}; //命令类型 UserCommand getUserInput() { return UserCommand::EditDocument; } //获取用户命令 string getFilenameFromUser() { return ""; } //获取文件名 void processUserInput(UserCommand cmd){} //处理其它命令 void editDocument(const std::string& fileName) { openDocAndDisplay(fileName); while (!doneEditing()) { UserCommand cmd = getUserInput(); if (cmd == UserCommand::OpenNewDocument) { //若是用户选择打开一个新文档 const string newName = getFilenameFromUser(); std::thread t(editDocument, newName); //启动新线程去处理这个新文档 t.detach(); //子线程分离。这样主线程就能够继续处理其余任务。 }else { processUserInput(cmd); } } } int main() { //1. 悬空引用问题 oops(); //2. 利用分离线程处理多文档文件 editDocument("E:\\Demo\\abc.doc"); return 0; }
二. std::thread对象的析构
(一)std::thread的析构
1. std::thread对象析构时,会先判断joinable(),若是可联结,则程序会直接被终止(terminate)。
2. 这意味std::thread对象从其它定义域出去的任何路径,都应为不可联结状态。也意味着建立thread对象之后,要在随后的某个地方显式地调用join或detach以便让std::thread处于不可联结状态。
(二)为何析构函数中不隐式调用join或detach?
1. 若是设计成隐式join():将致使调用线程一直等到子线程结束才返回。若是子线程正在运行一个耗时任务,这可能形成性能低下的问题,并且问题也不容易被发现。
2. 若是设计成隐式detach():因为detach会将切断std::thread对象与底层线程之间的关联,两个线程今后各自独立运行。若是线程函数是按引用(或指针)方式捕捉的变量,在调用线程退出做用域后这些变量会变为无效,这容易掩盖错误也将使调试更加困难。所以隐式detach,还不如join或者显式调用detach更直观和安全。
3.标准委员会认为,销毁一个joinable线程的后果是十分可怕的,所以他们经过terminate程序来禁止这种行为。为了不销毁一个joinable的线程,就得由程序员本身来确保std::thread对象从其定义的做用域出去的任何路径,都处于不可联结状态,最经常使用的方法就是资源获取即初始化技术(RAII,Resource Acquisition Is Initialization)。
(三)std::thread对象与RAII技术的结合
1. 方案1:自定义的thread_guard类,并将std::thread对象传入其中,同时在构造时选择join或detach策略。当thread_guard对象析构时,会根据析构策略,调用std::thread的join()或detach(),确保在任何路径,线程对象都处于unjoinable状态。
2. 方案2:从新封装std::thread类(见下面的代码,类名为joining_thread),在析构时隐式调用join()。
【编程实验】利用RAII确保std::thread全部路径皆为unjoinable
#include <iostream> #include <thread> #include <functional> #include <algorithm> using namespace std; constexpr auto tenMillion = 10000000; bool conditionsAreSatisfied() { return false;}//return true or false //问题函数:doWork_oops(没有确保std::thread全部皆为不可联结) //参数:filter过滤器,选0至maxVal之间的值选择出来并放入vector中 bool doWork_oops(std::function<bool(int)> filter, int maxVal = tenMillion) { std::vector<int> goodVals; //保存通过滤器筛选出来的数值(0-maxVal) std::thread t([&filter, maxVal, &goodVals] { //注意goodVals是局部变量,按引用传入子线程。 for (auto i = 0; i <= maxVal; ++i) if (filter(i)) goodVals.push_back(i); }); if (conditionsAreSatisfied()) { //若是一切就绪,就开始计算任务 t.join(); //等待子线程结束 //performComputation(goodVals); //主线程执行计算任务 return true; } //conditionsAreSatisfied()时false,表示条件不知足。(注意,仍没调用join()或detach()) return false; //调用线程(通常是主线程)执行到这里,t对象被析构,std::thread的析构函数被调用, //此时因为子线程仍处于可联结状态,将执行std::ternimate终止程序! //为何std::thread析构函数不隐式执行join或detach,而是终止程序的运行? //若是隐式调用join()会让主线程等待子线程(耗时任务)结束,这会浪费性能。 //而若是隐式调用detach会使主线程和子线程分离,子线程因为引用goodVals局部变量, //会出现悬空引用的问题,但这问题又不容易被发现。所以,经过std::ternimate来终止 //程序,以便让程序员本身决定和消除这些问题。好比继续调用join(),仍是detach(但需 //要同时解决悬空引用问题)? } //利用RAII技术,确保std::thread的正常析构 class thread_guard //scoped_thread { public: enum class DtorAction{join, detach}; //析构行为 //构造函数只接受右值类型,由于std::thread只能被移动。虽然t为右值引用类型,但因为形参自己 //左值,所以调用std::move将形参转为右值。 thread_guard(std::thread&& t, DtorAction a = DtorAction::join):action(a), thr(std::move(t)) { } ~thread_guard() { if (thr.joinable()) //必须校验,join和detach只能被调用一次 { if (action == DtorAction::join) { thr.join(); } else { thr.detach(); } } } std::thread& get() { return thr; } //因为声明了析构函数,编译器将再也不提供移动操做函数,所以需手动生成 thread_guard(thread_guard&&) noexcept = default; thread_guard& operator=(thread_guard&&) = default; //本类不支持复制 thread_guard(const thread_guard&) = delete; thread_guard& operator=(const thread_guard&) = delete; private: //注意action和thr的声明顺序,因为thr被建立之后会执行起来,必须 //保证action己被初始化。所以先声明action,再声明thr。 DtorAction action; std::thread thr; }; bool doWork_ok(std::function<bool(int)> filter, int maxVal = tenMillion) { std::vector<int> goodVals; std::thread t([&filter, maxVal, &goodVals] { //注意goodVals是局部变量,按引用传入子线程。 for (auto i = 0; i <= maxVal; ++i) if (filter(i)) { cout << i << endl; goodVals.push_back(i); } }); thread_guard guard(std::move(t));//默认析构策略是thread_guard::DtorAction::join if (conditionsAreSatisfied()) { //若是一切就绪,就开始计算任务 guard.get().join(); //等待子线程结束 //performComputation(goodVals); //主线程执行计算任务 return true; } //conditionsAreSatisfied()时false,表示条件不知足。guard对象析构,但会隐式调std::thread对象 //的join()。 return false; } //使用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() { //1.问题函数:doWork_oops:没有确保std::thread的全部路径都为joinable //doWork_oops([](auto val) { return val >= 100; }, 1000); //2. doWork_ok函数 doWork_ok([](auto val) { return val >= 100; }, 1000); //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; }