一. 线程的等待与分离

(一)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)注意事项:

    ①如果不等待线程,就必须保证线程结束之前,可访问的数据是有效的。特别是要注意线程函数是否还持有一些局部变量的指针或引用

    ②为防止上述的悬空指针和悬引用的问题,线程对象的生命期应尽量长于底层线程的生命期。

  (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 = ; j < ; ++j)
{
do_something(i); //可能出现悬空引用的问题。
}
}
}; void oops()
{
int localVar = ;
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 ;
}

二. 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 = ; 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 = ; 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 = ; 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 >= ; }, ); //3. 测试joining_thread类
std::vector<joining_thread> threads; //joining_thread析构时隐式调用join
for (unsigned int i = ; i < ; ++i) {
threads.push_back(joining_thread(doWork, i));
} std::for_each(threads.begin(), threads.end(), std::mem_fn(&joining_thread::join)); return ;
}

第25课 std::thread对象的析构的更多相关文章

  1. 第24课 std::thread线程类及传参问题

    一. std::thread类 (一)thread类摘要及分析 class thread { // class for observing and managing threads public: c ...

  2. C++ 多线程 std::thread 使用总结

    在C++ 11之前,官方并没有支持线程库.C++ 11通过标准库引入了对 thread 类的支持,大大方便了完成多线程开发的工作. std::thread 构造函数  (1)thread() noex ...

  3. std::thread

    std::shared_ptr<std::thread> m_spThread; m_spThread.reset(new std::thread(std::bind(&GameS ...

  4. C++11 并发指南------std::thread 详解

    参考: https://github.com/forhappy/Cplusplus-Concurrency-In-Practice/blob/master/zh/chapter3-Thread/Int ...

  5. std::thread使用

    本文将从以下三个部分介绍C++11标准中的thread类,本文主要内容为: 启动新线程 等待线程与分离线程 线程唯一标识符 1.启动线程 线程再std::threada对象创建时启动.最简单的情况下, ...

  6. C++11并发编程:多线程std::thread

    一:概述 C++11引入了thread类,大大降低了多线程使用的复杂度,原先使用多线程只能用系统的API,无法解决跨平台问题,一套代码平台移植,对应多线程代码也必须要修改.现在在C++11中只需使用语 ...

  7. C++ std::thread概念介绍

    C++ 11新标准中,正式的为该语言引入了多线程概念.新标准提供了一个线程库thread,通过创建一个thread对象来管理C++程序中的多线程. 本文简单聊一下C++多线程相关的一些概念及threa ...

  8. 第12课 std::bind和std::function(3)_std::function可调用对象包装器

    1. std::function (1)首先是一个类模板,用于包装可调用对象.可以容纳除了类成员(函数)指针之外的所有可调用对象. (2)可以将普通函数,lambda表达式和函数对象类统一起来.尽管它 ...

  9. C++11 并发之std::thread std::mutex

    https://www.cnblogs.com/whlook/p/6573659.html (https://www.cnblogs.com/lidabo/p/7852033.html) C++:线程 ...

随机推荐

  1. Centos7编译安装Nginx+keepalived

    一.安装环境.主机信息及软件版本 Nginx:1.12.2keepalived:2.0.12时间同步(同步后确认各服务器时间是否一致,不一致需要修改一下时区) 关闭防火墙 二.编译安装Nginx 1. ...

  2. 使用 PDBDownloader 解决 IDA 加载 ntoskrnl.exe 时符号不完全问题

    解决 IDA 加载 ntoskrnl.exe 时符号不完全问题 1. 问题:IDA加载xp系统的 ntoskrnl.exe 加载不完全. 2. 尝试过但未成功的解决方案: 1)配置好的IDA的 pdb ...

  3. duba网址对firefox快捷方式的劫持

    直接删除 “驱动精灵” 即可. 等我 二进制安全 学好了,一定开发一种病毒专干这种劫持的,煞笔软件.

  4. 引用kernel32.dll中的API来进行串口通讯

    串口通讯可以引出kernel32.dll中的API来操作,相关源码如下:using System;using System.Runtime.InteropServices; namespace Tel ...

  5. 纯C语言实现链栈

    #include <stdio.h> #include <stdlib.h> typedef int ElemType; typedef struct StackNode{ E ...

  6. mask-rcnn代码解读(七):display(self)函数的解析

    如和将class中定义的变量打印或读取出来,受maskrcnn的config.py的启示,我将对该函数进行解释. 我将介绍该函数前,需要对一些名词进行解释,如下: ①Ipython:ipython是一 ...

  7. [TCP/IP] TCP如何实现流量控制和拥塞控制

    流量控制:数据的传送与接收过程当中很可能出现收方来不及接收的情况,这时就需要对发方进行控制,以免数据丢失.流量控制用于防止在端口阻塞的情况下丢帧,这种方法是当发送或接收缓冲区开始溢出时通过将阻塞信号发 ...

  8. 【Linux】-- 认识bash shell

    一.前言 我们知道管理整个计算机硬件的其实是系统的内核,这个内核是需要被保护的,所以我们一般用户就只能通过shell来跟内核通信,以让内核达到我们所想要达到的工作.那么Linux系统有多少shell可 ...

  9. LNK2001 无法解析的外部符号 __imp__CameraCreateSettingPage@24

    用VS2017,Release X86进行编译时显示如下错误: 1>CWDMDlg.obj : error LNK2001: 无法解析的外部符号 __imp__CameraGetImageBuf ...

  10. idea中的插件,可以快速将类中的属性转换成Json字符串

    当我们想要测试接口的时候,难免会根据一个类,一个一个的写json数据,当属性比较少时还行,但当属性多的时候就比较麻烦了, 为了解决这个问题,我们可以安装第三方的插件来快速生成json字符串. 步骤如下 ...