Contents

多线程

创建

简单来说每个线程都和执行一个main函数是一样的(main就是一个线程,只不过这个线程中main是它的入口函数),也需要一个入口函数,并且在入口函数结束之后也会自动退出。

在为一个线程创建了一个std::thread对象后,需要等待这个线程结束。

#include <thread>

// 将do_some_work作为入口函数
void do_some_work();
std::thread my_thread(do_some_work);

// 传入类,可以对线程对象的符号函数进行重载
class background_task
{
public:
void operator()() const
{
    do_something();
    do_something_else();
}
};
background_task f;
std::thread my_thread(f);

// 传入lambda函数
std::thread my_thread([]{
  do_something();
  do_something_else();
});

// 传入struct
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. 新线程可能还在运行

附录

void operator()() const 解析

第一个()是运算符的名称 – 它是在对象上使用()时调用的运算符. 第二个()是用于参数的

#include <iostream>
 
class Test {
public:
    void operator()(int a, int b) {
        std::cout << a + b << std::endl;
    }
};
 
int main() {
    Test t;
    t(3,5);//相当于调用了operator()(int a, int b)
    return 0;
}
 
// 输出结果
// 8
lambda表达式

lambda表达式的一系列语义都需要封闭在括号中,还要以方括号作为前缀:

// 简单示例
[]{  // lambda表达式以[]开始
    do_stuff();
    do_more_stuff();
}();  // 表达式结束,可以直接调用

// 作为函数参数
std::vector<int> data=make_data();
std::for_each(data.begin(),data.end(),[](int i){std::cout<<i<<"\n";});

// 带return
std::condition_variable cond;
bool data_ready;
std::mutex m;
void wait_for_data()
{
    std::unique_lock<std::mutex> lk(m);
    cond.wait(lk,[]()->bool{
        if(data_ready)
        {
            std::cout<<Data ready<<std::endl;
            return true;
        }
        else
        {
            std::cout<<Data not ready, resuming wait<<std::endl;
            return false;
        }
    });
}

可以像下面这样显示指定返回类型:

[] (int x, int y) -> int { int z = x + y; return z; }

获取变量

lambda函数使用空的[](lambda introducer)就不能引用当前范围内的本地变量;其只能使用全局变量,或将其他值以参数的形式进行传递。当想要访问一个本地变量,需要对其进行捕获(拷贝)。最简单的方式就是将范围内的所有本地变量都进行捕获,使用**[=]**就可以完成这样的功能。函数被创建的时候,就能对本地变量的副本进行访问了。

// 注意,以下函数的返回值是lambda函数类型std::function,模板函数<int(int)>
// 括号里是传入参数类型,括号外是lambda返回值类型参数
std::function<int(int)> make_offseter(int offset)
{
    return [=](int j){return offset+j;};
}

// 调用
int main()
{
  std::function<int(int)> offset_42=make_offseter(42);
  std::function<int(int)> offset_123=make_offseter(123);
  std::cout<<offset_42(12)<<,<<offset_123(12)<<std::endl;
  std::cout<<offset_42(12)<<,<<offset_123(12)<<std::endl;
}

使用**[&]**对所有本地变量进行引用(不进行拷贝,所以获得的变量值为当前环境的值)

int main()
{
  int offset=42;  // 1
  std::function<int(int)> offset_a=[&](int j){return offset+j;};  // 2
  offset=123;  // 3
  std::function<int(int)> offset_b=[&](int j){return offset+j;};  // 4
  std::cout<<offset_a(12)<<”,”<<offset_b(12)<<std::endl;  // 5
  offset=99;  // 6
  std::cout<<offset_a(12)<<”,”<<offset_b(12)<<std::endl;  // 7
}

join或detach

先给一个不好的例子,在detach之后,主线程结束,而子线程还在运行,并且访问了主线程的变量,导致程序崩溃

// 传入struct
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时,一定要确保不会用主线程的变量

join

如果需要等待线程,相关的std::thread实例需要使用join()。使用my_thread.join(),就可以确保局部变量在线程完成后,才被销毁。

确保在主线程异常退出时,还可以执行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

在这段代码中,拷贝构造函数和拷贝赋值操作被标记为=delete,是为了不让编译器自动生成它们。直接对一个对象进行拷贝或赋值是危险的,因为这可能会弄丢已经加入的线程。通过删除声明,任何尝试给thread_guard对象赋值的操作都会引发一个编译错误。

detach

调用std::thread成员函数detach()来分离一个线程。之后,相应的std::thread对象就与实际执行的线程无关了,并且这个线程也无法加入:

std::thread t(do_background_work);
t.detach();
assert(!t.joinable());

为了从std::thread对象中分离线程(前提是有可进行分离的线程),不能对没有执行线程的std::thread对象使用detach(),也是join()的使用条件,并且要用同样的方式进行检查——当std::thread对象使用t.joinable()返回的是true,就可以使用t.detach()。

附录

第2章 线程管理 - 2.1 线程管理的基础 - 《C++并发编程(中文版)(C++ Concurrency In Action)》 - 书栈网 · BookStack