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 对象的函数,而非启动了一个
线程。

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

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

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

struct func
{
int& i;
func(int& i_) : i(i_) {}
void operator() ()
{
for (unsigned j=0 ; j<1000000 ; ++j)
{
do_something(i);
// 1. 潜在访问隐患:悬空引用
}
}
};
void oops()
{
int some_local_state=0;
func my_func(some_local_state);
std::thread my_thread(my_func);
my_thread.detach();
}
// 2. 不等待线程结束
// 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 异常情况下的等待 
struct func; // 定义在代码2.1中
void f()
{
  int some_local_state=0;
  func my_func(some_local_state);
  std::thread t(my_func);
  try
  {
    do_something_in_current_thread();
  }
  catch(...)
  {
    t.join();  // 1
    throw;
  }
  t.join();  // 2
}

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

class thread_guard
{
  std::thread& t;
public:
  explicit thread_guard(std::thread& t_):
    t(t_)
  {}
  ~thread_guard()
  {
    if(t.joinable()) // 1
    {
      t.join();      // 2
    }
  }
  thread_guard(thread_guard const&)=delete;   // 3
  thread_guard& operator=(thread_guard const&)=delete;
};

struct func; // 定义在代码2.1中

void f()
{
  int some_local_state=0;
  func my_func(some_local_state);
  std::thread t(my_func);
  thread_guard g(t);
  do_something_in_current_thread();
}    // 4
2.1.4 后台运行线程

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

2.2 向线程函数传递参数 

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

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

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

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

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);
}

内部代码会将拷贝的参数以右值的方式进行传递,这是为了那些只支持移动的类型,而后会尝试以右值为实参调用update_data_for_widget。但因为函数期望的是一个非常量引用作为参数(而非右值),所以会在编译时出错。

问题的解决办法很简单:可以使用std::ref将参数转换成引用的形式。因此可将线程的调用改为以下形式:

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

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

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

class X
{
public:
  void do_lengthy_work();
};
X my_x;
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是如何转移动态对象的所有权到线程中去的:

void process_big_object(std::unique_ptr);

std::unique_ptr p(new big_object);
p->prepare_data(42);
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 移交线程归属权

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

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

void do_work(unsigned id);

void f()
{
  std::vector threads;
  for (unsigned i = 0; i < 20; ++i)
  {
    threads.emplace_back(do_work,i); // 产生线程
  } 
  for (auto& entry : threads) // 对每个线程调用 join()
    entry.join();       
}

2.4 在运行时选择线程数量

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

template
struct accumulate_block
{
  void operator()(Iterator first,Iterator last,T& result)
  {
    result=std::accumulate(first,last,result);
  }
};

template
T parallel_accumulate(Iterator first,Iterator last,T init)
{
  unsigned long const length=std::distance(first,last);

  if(!length) // 1
    return init;

  unsigned long const min_per_thread=25;
  unsigned long const max_threads=
      (length+min_per_thread-1)/min_per_thread; // 2

  unsigned long const hardware_threads=
      std::thread::hardware_concurrency();

  unsigned long const num_threads=  // 3
      std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);

  unsigned long const block_size=length/num_threads; // 4

  std::vector results(num_threads);
  std::vector threads(num_threads-1);  // 5

  Iterator block_start=first;
  for(unsigned long i=0; i < (num_threads-1); ++i)
  {
    Iterator block_end=block_start;
    std::advance(block_end,block_size);  // 6
    threads[i]=std::thread(     // 7
        accumulate_block(),
        block_start,block_end,std::ref(results[i]));
    block_start=block_end;  // 8
  }
  accumulate_block()(
      block_start,last,results[num_threads-1]); // 9
      
  for (auto& entry : threads)
    entry.join();  // 10

  return std::accumulate(results.begin(),results.end(),init); // 11
}

因为不能直接从一个线程中返回值,所以需要传递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相同。

std::thread::id master_thread;
void some_core_part_of_algorithm()
{
  if(std::this_thread::get_id()==master_thread)
  {
    do_master_thread_work();
  }
  do_common_work();
}

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);
}

你可能感兴趣的:(c++,算法,开发语言,C++并发编程)