• 线程的基本操作(二)


    传递参数

    向可调用对象或函数传递参数很简单,只需要将这些参数作为std::thread构造函数的附加参数即可,需要注意的是,这些参数会拷贝至新线程的内存空间中(同临时变量一样),即使函数中的参数是引用的形式,拷贝操作也会执行。举一个例子:

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

    代码创建了一个调用f(3, "hello")的线程。注意,函数f需要一个std::string对象作为第二个参数,但这里使用的是字符串的字面值,也就是char const *类型,线程的上下文完成字面值向std::string的转化。需要特别注意,指向动态变量的指针作为参数的情况,代码如下:

    1. void f(int i, std::string const& s);
    2. void oops(int some_para)
    3. {
    4. char buffer[1024]; //1
    5. sprintf_s(buffer, "%i", some_para);
    6. std::thread t(f, 3, buffer); //2
    7. t.detach();
    8. }

    buffer①是一个指针变量,指向局部变量,然后此局部变量通过buffer传递到新线程中②。此时,函数oops可能会在buffer转换成std::string之前结束,从而导致未定义的行为。因为,无法保证隐式转换的操作和std::thread构造函数的拷贝操作的顺序,有可能std::thread的构造函数拷贝的是转换前的变量(buffer指针)。解决方案就是在传递到std::thread构造函数之前,就将字面值转化为std::string

    1. void f(int i,std::string const& s);
    2. void not_oops(int some_param)
    3. {
    4. char buffer[1024];
    5. sprintf_s(buffer,"%i", some_param);
    6. std::thread t(f, 3, std::string(buffer)); // 使用std::string,避免悬空指针
    7. t.detach();
    8. }

    相反的情形(期望传递一个非常量引用,但复制了整个对象)倒是不会出现,因为会出现编译错误。比如,尝试使用线程更新引用传递的数据结构:

    1. void update_data_for_widget(widget_id w, widget_data& data); // 1
    2. void oops_again(widget_id w)
    3. {
    4. widget_data data;
    5. std::thread t(update_data_for_widget, w, data); // 2
    6. display_status();
    7. t.join();
    8. process_widget_data(data);
    9. }

    虽然update_data_for_widget①的第二个参数期待传入一个引用,但std::thread的构造函数②并不知晓,构造函数无视函数参数类型,盲目地拷贝已提供的变量。不过,内部代码会将拷贝的参数以右值的方式进行传递,这是为了那些只支持移动的类型,而后会尝试以右值为实参调用update_data_for_widget。但因为函数期望的是一个非常量引用作为参数(而非右值),所以会在编译时出错。对于熟悉std::bind的开发者来说,问题的解决办法很简单:可以使用std::ref将参数转换成引用的形式。因此可将线程的调用改为以下形式:

    std::thread t(update_data_for_widget,w,std::ref(data));

    这样update_data_for_widget就会收到data的引用,而非data的拷贝副本,这样代码就能顺利的通过编译了。

    如果熟悉std::bind,就应该不会对以上述传参的语法感到陌生,因为std::thread构造函数和std::bind的操作在标准库中以相同的机制进行定义。比如,你也可以传递一个成员函数指针作为线程函数,并提供一个合适的对象指针作为第一个参数:

    1. class X
    2. {
    3. public:
    4. void do_length_work()
    5. {
    6. std::cout << "do_length_work" << std::endl;
    7. }
    8. };
    9. X my_x;
    10. std::thread xt(&X::do_length_work, &my_x); //1
    11. xt.join();

    这段代码中,新线程将会调用my_x.do_length_work(),其中my_x的地址①作为对象指针提供给函数。也可以为成员函数提供参数:std::thread构造函数的第三个参数就是成员函数的第一个参数,以此类推:

    1. class X
    2. {
    3. public:
    4. void do_length_work()
    5. {
    6. std::cout << "do_length_work" << std::endl;
    7. }
    8. void do_length_work_para(std::string para)
    9. {
    10. std::cout << "do_length_work_para--" << para << std::endl;
    11. }
    12. };
    13. X my_x1;
    14. std::thread xt1(&X::do_length_work_para, &my_x1, "hello");
    15. xt1.join();

    另一种有趣的情形是,提供的参数仅支持移动(move),不能拷贝。“移动”是指原始对象中的数据所有权转移给另一对象,从而这些数据就不再在原始对象中保存,std::unique_ptr就是这样一种类型,这种类型为动态分配的对象提供内存自动管理机制。同一时间内,只允许一个std::unique_ptr实例指向一个对象,并且当这个实例销毁时,指向的对象也将被删除。移动构造函数和移动赋值操作符允许一个对象的所有权在多个unique_ptr实例中传递。使用“移动”转移对象所有权后,就会留下一个空指针。使用移动操作可以将对象转换成函数可接受的实参类型,或满足函数返回值类型要求。当原对象是临时变量时,则自动进行移动操作,但当原对象是一个命名变量,转移的时候就需要使用std::move()进行显示移动。下面的代码展示std::move的用法,展示了std::move是如何转移动态对象的所有权到线程中去的:

    1. void process_big_object(std::unique_ptr);
    2. std::unique_ptr p(new big_object);
    3. p->prepare_data(42);
    4. std::thread t(process_big_object, std::move(p));

    通过在std::thread构造函数中执行std::move(p),big_object对象的所有权首先被转移到新创建线程的内部存储中,之后再传递给process_big_object函数。

    C++标准线程库中和std::unique_ptr在所属权上相似的类有好多种,std::thread为其中之一。虽然,std::thread不像std::unique_ptr能占有动态对象的所有权,但在它能占有其他资源:每个实例都负责管理一个线程。线程的所有权可以在多个std::thread实例中转移,这依赖于std:thread实例的可移动且不可复制性。不可复制性表示在某一时间点,一个std::thread实例只能关联一个执行线程。可移动性使得开发者可以自己决定,哪个实例拥有线程实际执行的所有权。

  • 相关阅读:
    文件操作【c语言】
    Codeforces-1696 D: Permutation Graph【构造、分治、数据结构】
    docker全家桶(基本命令、dockerhub、docker-compose)
    为什么要用JMH?何时应该用?
    进程通信的方式
    知识图谱从入门到应用——知识图谱的存储与查询:基于原生图数据库的知识图谱存储
    webGL学习
    开放式耳机怎么选择、300之内最好的耳机推荐
    解决Docker容器apt无法下载问题
    Linux——进程控制(一)进程的创建与退出
  • 原文地址:https://blog.csdn.net/u012069234/article/details/126921575