std::thread相关总结

目录

一.基本线程管理

1.1 启动线程

1.2 等待线程完成

1.3 在异常环境下的等待

1.4 在后台运行线程

二.传递参数给线程函数

 

一.基本线程管理

1.1 启动线程

        启动线程是通过构造std::thread对象来完成的,该对象指定了线程上要完成的任务。在最简单的情况下,该任务仅仅是一个普普通通的返回void且不接受参数的函数。下面的f()是一个普通的函数,通过std::thread来进行创建线程,并启动。

void f(){
//函数体
}
std::thread t(f());

1.2 等待线程完成

        如果你需要等待线程完成,可以通过在相关联的std::thread实例上调用join()实现。与detach()进行区分,detach()是不等待线程完成后。

        join()很简单也很粗暴——要么等待一个线程完成要么不等。如果你需要对待等待的线程进行更细粒度的控制,比如检查线程是否完成,或只是在一段特定的时间内进行等待,那么就必须使用替代机制,例如条件变量和 future,我们将在后续中提到。调用ioin()的行为也会清理所有与该线程相关联的存储器,这样std::thread 对象不再与现已完成的线程相关联,它也不与任何线程相关联。这就意味着,你只能对一个给定的线程调用一次join(),一旦你调用了join(),此std::thread对象不再是可连接的,并且joinable()将返回 false。

1.3 在异常环境下的等待

         如前所述,你要确保在std::thread 对象被销毁前已调用join()或detach()函数。如果要分离线程,通常在线程启动后就可以立即调用 detach(),所以这不是个问题。但是如果打算等待该线程,就需要仔细地选择在代码的哪个位置调用join()这意味着,如果在线程开始之后但又是在调用 join()之前引发了异常,对join()的调用就容易被跳过。
        为了避免应用程序在引发异常的时候被终止,你需要在这种情况决定要做什么。一般来说,如果你打算在非异常的情况下调用 join(),你还需要在存在异常时调用join(),以避免意外的生命周期问题。下面展示了这样的简单代码。

struct func;
void f()
{
    int some local state=0;
    func my_func(some local state);
    std::thread t(my_func);
    try{
    do_something_incurrent_thread();
    }
    catch(...){
       t.join();
       throw;
    }
    t.join();
}

        上面的代码使用了tr/catch块,以确保访问局部状态的线程在函数退出前结束,无论函数是正常退出e还是异常中断。使用 try/catch 块很烦琐,而且容易将作用域弄乱,所以并不是一个理想的方案。如果确保线程必须在函数退出前完成是很重要的——无论是因为它具有对其他局部变量的引用还是任何其他原因一-那么确保这是所有可能的退出路径的情况是很重要的,无论正常还是异常,并且希望提供一个这样做的简单明了的机制。
        这样做的方法之一是使用标准的资源获取即初始化(RAII)惯用语法,并提供一个类,在它的析构函数中执行join()。下面给出一个这样的做法。

class thread_guard{
    std::thread& t;
public:
        explicit thread_guard(std::thread& t):t(t)
        ~thread_guard(){
            if(t.joinable()){
                t.join();
            }
        thread guard(thread guard const&)=delete;
        thread quard& 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(t);
    do_something_incurrent_thread();
}

        上面代码中,在当前线程的执行到达f末尾时,局部对象会按照构造函数的逆序被销毁。因此,thread_guard 对象g 首先被销毁,并且析构函数中线程被结合。即便是当函数因 do_something_incurrent_thread引发异常而退出的情况下也会发生。
在析构函数在调用join()e前,首先测试thread guard的析构函数是不是joinable()的。这很重要,因为对于一个给定的执行线程join()只能被调用一次,所以如果线程已经被结合,这样做就是错误的。
拷贝构造函数和拷贝赋值运算符被标记=delete,以确保他们不会由编译器自动提供。复制或赋值这样一个对象可能是危险的,因为它可能比它要结合的线程的作用域存在得更久。通过将它们声明为已删除的,任何复制 thread guard 对象的企图都将产生编译错误。
        如果无需等待线程完成,可以通过分离(detaching)它来避免这种异常安全问题。这打破了线程与 std::thread 对象的联系并确保当 std::thread 对象被销毁时,std::terminate()不会被调用,即使线程仍在后台运行。

1.4 在后台运行线程

        在std::thread对象上调用 detach()会把线程丢在后台运行也没有直接的方法与之通信。也不再可能等待该线程完成,如果一个线程成为分离的,获取一个引用它的 std::thread 对象也是不可能的,所以它也不再能够被结合。分离的线程确实是在后台运行,所有权和控制权被转交给C++运行时库,以确保与线程相关联的资源在线程退出后能够被正确地回收。
        参照UNIX的守护进程(daemonprocess)概念,被分离的线程通常被称为守护线程(daemon threads),它们无需任何显式的用户界面,而运行在后台。这样的线程通常是长时间运行的,它们可能在应用程序的几乎整个生命周期中都在运行,执行后台任务,例如监控文件系统、清除对象缓存中的未使用项或是优化数据结构。在另一个极端,有另一种鉴别线程何时完成的机制,或者线程被用作“即用即忘”任务,在这里使用分离线程也是有意义的。

        你在前面已经看到的,你通过调用 std::thread 对象的detach()的成员函数来分离线程。在调用完成后,std::thread 对象不再与执行的实际线程相关联同时也不能够被加入。

std::thread t(do background work);
t.detach();
assert(!t.joinable());


        为了从一个 std::thread 对象中分离线程,必须有一个线程供分离。你不能在一个没有与执行线程相关联的 std::thread 对象上调用detach()。这对于join()也是同样的要求,你可以用完全相同的方法进行检查一一你只能在 t.joinable()返回true的时候,为一个std::thread 对象t调用t.detach()。

        考虑一个类似于字处理器的应用程序,它可以一次编辑多个文档。有许多种方法在UI级别和内部来处理这个问题。有一种现在看起来越来越普遍的方式,是具有多个相互独立的顶层窗口,与正在编辑的文档一一对应。尽管这些窗口看起来完全独立,各自拥有自己的菜单等,但它们是在同一个应用程序的实例上运行的。一种在内部处理这个问题的方式是在其自己的线程中运行各自的文档编辑窗口,每个线程都运行相同的代码,但拥有与被编辑文档相关的不同的数据以及相应的窗口属性。打开一个新的文档就需要启动一个新的线程。处理请求的线程并不在乎等待其他的线程完成,因为它在一个不相关的文件上工作,所以运行分离的线程就成为了首选。

void edit document(std::string const& filename)
{
    open_document_and_display_gui(filename);
    while(!done_editing())
    user_command cmd=get_user_input();
    if(cmdtype==open_new_document){
        std::string const new_name=get_filename_from_user();
        std::thread t(edit_document,new_name)
        t.detach();
    }    
    else{
        process_user_input(cmd);
    }
}

        如果用户选择打开一个新的文档,它会提示其有文档要打开,启动新线程来打开该文档,然后分离它。因为新的线程与当前线程做着同样的操作,只是文件不同,你可以用新选定的文件名作为参数,重用同一个函数(edit document)。这个例子还展示了一个案例,它有助于传递参数给用来启动线程的函数:并非仅仅将函数名传递给 std::thread 构造函数,你还可以传递文件名参数。虽然也有其他机制能够做到这一点,例如使用具有成员数据的函数对象取代普通的带有参数的函数,但线程库提供了一个简单方法来实现之。

二.传递参数给线程函数

        传递参数给可调用对象或函数,基本上就是简单地将额外的参数传递给 std::thread 构造函数。但重要的是,参数会以默认的方式被复制(copied)到内部存储空间,在那里新创建的执行线程可以访问它们,即便函数中的相应参数期待着引用。这里有一个简单的例子。

void f(int i,std::string const& s);
std::thread t(f,3,“hello");

        这里创建一个新的与t相关联的执行线程,称为 (3,"hello")。注意即使 f接受 std::string作为第二个参数,字符串字面值仅在新线程的上下文中才会作为char const*传送,并转换为 std::string。尤其重要的是当提供的参数是一个自动变量的指针时,如下所示。

void f(int i,std::string const& s);
void oops(int some_param){
    char buffer[1024];
    sprintf(buffer,"%i",some_param);
    std::threadt(f,3,buffer);
    t.detach();
}

        在这种情况下,正是局部变量 buffer的指针被传递给新线程,还有一个重要的时机,即函数oops会在缓冲在新线程上被转换为 std::string之前退出,从而导致未定义的行为。解决之道是在将缓冲传递给 std::thread 的构造函数之前转换为std::string。

void f(inti,std::string const& s);
void not_oops(int some_param){
    char buffer[1024];
//使用std::string避免悬浮指针
    sprintf(buffer,"i",some_param);
    std::thread t(f,3,std::string(buffer));
    t.detach();
}

        在这种情况下,问题就出在你依赖从缓冲的指针到函数所期望的std::string对象的隐式转换,因为 std::thread 构造函数原样复制了所提供的值,并未转换为期望的参数类型。
        也有可能得到相反的情况,对象被复制,而你想要的是引用。这可能发生在当线程正在更新一个通过引用传递来的数据结构时,例如,

void update_data _for_widget(widget_id w,widget_data& data);
void oops_again(widget_id w){
    widget_data data;
    std::thread t(update_data_for_widget,w,data);
    display_status();
    it.join();
    process_widget_data(data);
}

        尽管update_data_for_widget希望通过引用传递第二个参数,std;:thread的构造函数却并不知道,它无视函数所期望的类型,并且盲目地复制了所提供的值。当它调用 update_data_for_widget 时,它最后将传递data在内部的副本的引用而非对data自身的引用。于是,当线程完成时,随着所提供参数的内部副本的销毁,这些改动都将被舍弃,将会传递一个未改变的 data,而非正确更新的版本给process_widget_data。对于熟悉std::bind的人来说,解决方案也是显而易见的。你需要用 std::ref 来包装确实需要被引用的参数。在这种情况下,如果你将对线程的调用改为

std::thread t(update_data_for_widget,w,std::ref(data));
  • std::bind:使用的是参数的拷贝而不是引用,当可调用对象期待入参为引用时,必须显式利用std::ref来进行引用绑定。使用std::bind可以将可调用对象和参数一起绑定,绑定后的结果使用std::function进行保存,并延迟调用到任何我们需要的时候。
  • 多线程std::thread的可调用对象期望入参为引用时,也必须显式通过std::ref来绑定引用进行传参。
  • std::ref:用于取某个变量的引用,引入其是为了解决函数式编程(如std::bind)的一些传参问题。

        那么update_data_for_widget 将被正确地传data的引用,而非data副本(copy)的引用。
        如果你熟悉std::bind,那么参数传递语义就不足为奇,因为 std::thread构造函数和 std::bind 的操作都是依据相同的机制定义的。这意味着,例如,你可以传递一个成员函数的指针作为函数,前提是提供一个合适的对象指针作为第一个参数。 

class X {
public:
    void do_lengthy_work();
}
X my_x;
std::thread t(&x::do_lengthy_work,&my_x);

         这段代码将在新线程上调用my_x.do_lengthy_work(),因为my_x的地址是作为对象指针提供的。你也可以提供参数给这样的成员函数调用:std::thread 构造函数的第三个参数将作为成员函数的第一个参数等等。

        提供参数的另一个有趣的场景是,这里的参数不能被复制但只能被移动(moved):一个对象内保存的数据被转移到另一个对象,使原来的对象变成“空壳”。这种类型的一个例子是 std::unique_ ptr,它提供了动态分配对象的自动内存管理。只有一个std::unique_ptr实例可以在某一时刻指向一个给定的对象,当该实例被销毁时,其指向的对象将被删除。移动构造函数(move constructor)移动赋值运算符(moveassignmentoperator)允许一个对象的所有权在std::uniqueptr实例之间进行转移(参见附录A中A.1.1节,关于移动语的详情)。这种转移给源对象留下一个NULL指针。这种值的移动使得该类型的对象作为函数的参数被接受或从函数返回值。在源对象是临时的场合,这种移动是自动的,但在源是一个命名值的地方,此转移必须直接通过调用 std::move()来请求。下面的示例展示了运用 std::move将动态对象的所有权转移到一个线程中。

void process_big_object(std::unique_ptr<big_object>);
std::unique_ptr<big_object> p(new_big_object);
p->prepare_data(42);
std::thread t(process_big_object,std::move(p));

        通过在std::thread构造函数中指定std::move(p),big_object的所有权先被转移进新创建的线程的内部存储中,然后进入process_big_object。标准线程库中的一些类表现出与 std::unique_ptr相同的所有权语义,std::thread 就是其中之一。虽然 std::thread 实例并不拥有与 std::unique_ptr同样方式的动态对象,但他们却拥有资源,每一个实例负责管理一个执行线程。这种所有权可以在实例之间进行转移,因为 std::thread 的实例是可移动的(movable),即使他们不是可复制的(copyable)这确保了在允许程序员选择在对象之间转换所有权的时候,在任意时刻只有一个对象与某个特定的执行线程相关联

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值