• C++并发编程实战 第二版 第二章


    目录

    2.1 线程的基本管控

    2.1.2 等待线程完成

    2.1.3 异常情况下的等待 

    2.1.4 后台运行线程

    2.2 向线程函数传递参数 

    2.3 移交线程归属权

    2.4 在运行时选择线程数量

    2.5 识别线程


     

    参考:https://github.com/xiaoweiChen/CPP-Concurrency-In-Action-2ed-2019/blob/master/content/chapter2/2.1-chinese.md 

    2.1 线程的基本管控

    2.1.1 发起线程

    1.有件事需要注意,当把函数对象传入到线程构造函数中时,需要避免“最令人头痛的语法解
    析”(C++’s most vexing parse, 中文简介)。如果你传递了一个临时变量,而不是一个命名的变
    量;C++编译器会将其解析为函数声明,而不是类型对象的定义。

    std::thread my_thread(background_task());

    这里相当与声明了一个名为my_thread的函数,这个函数带有一个参数(函数指针指向没有参
    数并返回background_task对象的函数),返回一个 std::thread 对象的函数,而非启动了一个
    线程。

    使用多组括号①,或使用新统一的初始化语法②,可以避免这个问题。

    1. std::thread my_thread((background_task()));// 1
    2. std::thread my_thread{background_task()};// 2

    2.函数已经结束,线程依旧访问局部变量:

    1. struct func
    2. {
    3. int& i;
    4. func(int& i_) : i(i_) {}
    5. void operator() ()
    6. {
    7. for (unsigned j=0 ; j<1000000 ; ++j)
    8. {
    9. do_something(i);
    10. // 1. 潜在访问隐患:悬空引用
    11. }
    12. }
    13. };
    14. void oops()
    15. {
    16. int some_local_state=0;
    17. func my_func(some_local_state);
    18. std::thread my_thread(my_func);
    19. my_thread.detach();
    20. }
    21. // 2. 不等待线程结束
    22. // 3. 新线程可能还在运行

    这个例子中,已经决定不等待线程结束(使用了detach() ② ),所以当oops()函数执行完成时③,新线程中的函数可能还在运行。如果线程还在运行,它就会去调用do_something(i)函数①,这时就会访问已经销毁的变量。如同一个单线程程序——允许在函数完成后继续持有局部变量的指针或引用;当然,这从来就不是一个好主意——这种情况发生时,错误并不明显,会使多线程更容易出错。

    处理这种情况的常规方法:使线程函数的功能齐全,将数据复制到线程中,而非复制到共享数据中。

    此外,可以通过join()函数来确保线程在函数完成前结束。

    2.1.2 等待线程完成

    清单2.1中,将my_thread.detach()替换my_thread.join(),就可以确保局部变量在线程完成后,才被销毁。

    只能对一个线程使用一次join();一旦已经使用过join(),
    std::thread对象就不能再次加入了,当对其使用joinable()时,将返回false。

    2.1.3 异常情况下的等待 
    1. struct func; // 定义在代码2.1中
    2. void f()
    3. {
    4. int some_local_state=0;
    5. func my_func(some_local_state);
    6. std::thread t(my_func);
    7. try
    8. {
    9. do_something_in_current_thread();
    10. }
    11. catch(...)
    12. {
    13. t.join(); // 1
    14. throw;
    15. }
    16. t.join(); // 2
    17. }

    一种方式是使用“资源获取即初始化方式”(RAII,Resource Acquisition Is Initialization),提供一个类,在析构函数中使用join()。如同下面代码。

    1. class thread_guard
    2. {
    3. std::thread& t;
    4. public:
    5. explicit thread_guard(std::thread& t_):
    6. t(t_)
    7. {}
    8. ~thread_guard()
    9. {
    10. if(t.joinable()) // 1
    11. {
    12. t.join(); // 2
    13. }
    14. }
    15. thread_guard(thread_guard const&)=delete; // 3
    16. thread_guard& operator=(thread_guard const&)=delete;
    17. };
    18. struct func; // 定义在代码2.1中
    19. void f()
    20. {
    21. int some_local_state=0;
    22. func my_func(some_local_state);
    23. std::thread t(my_func);
    24. thread_guard g(t);
    25. do_something_in_current_thread();
    26. } // 4
    2.1.4 后台运行线程

    使用detach()会让线程在后台运行,这就意味着主线程不能与之产生直接交互。也就是说,不会等待这个线程结束。

    2.2 向线程函数传递参数 

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

    需要特别注意,指向动态变量的指针作为参数的情况:

    1. void f(int i,std::string const& s);
    2. void oops(int some_param)
    3. {
    4. char buffer[1024]; // 1
    5. sprintf(buffer, "%i",some_param);
    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 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. }

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

    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::ref将参数转换成引用的形式。因此可将线程的调用改为以下形式:

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

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

    也可以传递一个成员函数指针作为线程函数,并提供一个合适的对象指针作为第一个参数:

    1. class X
    2. {
    3. public:
    4. void do_lengthy_work();
    5. };
    6. X my_x;
    7. std::thread t(&X::do_lengthy_work, &my_x); // 1

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

    另一种有趣的情形是,提供的参数仅支持移动(move),当原对象是临时变量时,则自动进行移动操作,但当原对象是一个命名变量,转移的时候就需要使用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));

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

    2.3 移交线程归属权

    1. void some_function();
    2. void some_other_function();
    3. std::thread t1(some_function); // 1
    4. std::thread t2=std::move(t1); // 2
    5. t1=std::thread(some_other_function); // 3
    6. std::thread t3; // 4
    7. t3=std::move(t2); // 5
    8. t1=std::move(t3); // 6 赋值操作将使程序崩溃

    代码2.8 生成多个线程,并等待它们完成运行:

    1. void do_work(unsigned id);
    2. void f()
    3. {
    4. std::vector threads;
    5. for (unsigned i = 0; i < 20; ++i)
    6. {
    7. threads.emplace_back(do_work,i); // 产生线程
    8. }
    9. for (auto& entry : threads) // 对每个线程调用 join()
    10. entry.join();
    11. }

    2.4 在运行时选择线程数量

    代码2.9 并行版的std::accumulate:

    1. template<typename Iterator,typename T>
    2. struct accumulate_block
    3. {
    4. void operator()(Iterator first,Iterator last,T& result)
    5. {
    6. result=std::accumulate(first,last,result);
    7. }
    8. };
    9. template<typename Iterator,typename T>
    10. T parallel_accumulate(Iterator first,Iterator last,T init)
    11. {
    12. unsigned long const length=std::distance(first,last);
    13. if(!length) // 1
    14. return init;
    15. unsigned long const min_per_thread=25;
    16. unsigned long const max_threads=
    17. (length+min_per_thread-1)/min_per_thread; // 2
    18. unsigned long const hardware_threads=
    19. std::thread::hardware_concurrency();
    20. unsigned long const num_threads= // 3
    21. std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);
    22. unsigned long const block_size=length/num_threads; // 4
    23. std::vector results(num_threads);
    24. std::vector threads(num_threads-1); // 5
    25. Iterator block_start=first;
    26. for(unsigned long i=0; i < (num_threads-1); ++i)
    27. {
    28. Iterator block_end=block_start;
    29. std::advance(block_end,block_size); // 6
    30. threads[i]=std::thread( // 7
    31. accumulate_block(),
    32. block_start,block_end,std::ref(results[i]));
    33. block_start=block_end; // 8
    34. }
    35. accumulate_block()(
    36. block_start,last,results[num_threads-1]); // 9
    37. for (auto& entry : threads)
    38. entry.join(); // 10
    39. return std::accumulate(results.begin(),results.end(),init); // 11
    40. }

    因为不能直接从一个线程中返回值,所以需要传递results容器的引用到线程中去。另一个办法,通过地址来获取线程执行的结果(第4章中,我们将使用future完成这种方案)。

    2.5 识别线程

    线程标识为std::thread::id类型,可以通过两种方式进行检索。第一种,可以通过调用std::thread对象的成员函数get_id()来直接获取。如果std::thread对象没有与任何执行线程相关联,get_id()将返回std::thread::type默认构造值,这个值表示“无线程”。第二种,当前线程中调用std::this_thread::get_id()(这个函数定义在头文件中)也可以获得线程标识。

    std::thread::id实例常用作检测线程是否需要进行一些操作。比如:当用线程来分割一项工作(如代码2.9),主线程可能要做一些与其他线程不同的工作,启动其他线程前,可以通过std::this_thread::get_id()得到自己的线程ID。每个线程都要检查一下,其拥有的线程ID是否与初始线程的ID相同。

    1. std::thread::id master_thread;
    2. void some_core_part_of_algorithm()
    3. {
    4. if(std::this_thread::get_id()==master_thread)
    5. {
    6. do_master_thread_work();
    7. }
    8. do_common_work();
    9. }

    void update_data_for_widget(widget_id w,widget_data& data); // 1
    void oops_again(widget_id w)
    {
      widget_data data;
      std::thread t(update_data_for_widget,w,data); // 2
      display_status();
      t.join();
      process_widget_data(data);
    }void update_data_for_widget(widget_id w,widget_data& data); // 1
    void oops_again(widget_id w)
    {
      widget_data data;
      std::thread t(update_data_for_widget,w,data); // 2
      display_status();
      t.join();
      process_widget_data(data);
    }

  • 相关阅读:
    SpringBoot-42-注册Web原生组件
    CDH安装步骤
    maven配置jib-maven-plugin插件构建java应用docker镜像
    upp(统一流程平台)项目范围说明书
    202206-3 CCF 角色授权 (运用stl容器模拟 + 优化 满分题解)
    WPF中动画
    【前端】Vue+Element UI案例:通用后台管理系统-Echarts图表准备:axios封装、mock数据模拟实战
    Linux系统性能监测工具——负载/内存/磁盘
    详解SPI
    【论文精读】Robust Alignment for Panoramic Stitching Via an Exact Rank Constraint
  • 原文地址:https://blog.csdn.net/qq_52758467/article/details/133270049