Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

学习自 第25课 std::thread对象的析构 - 浅墨浓香 - 博客园 (cnblogs.com)和《c++并发编程实践第二版》2.1

一、线程创建

std::thread th对象创建时,创建线程,这个线程是主线程的子线程,与主线程并发执行

语法

  • 每个程序有一个执行 main() 函数的主线程,将函数添加为 std::thread 的参数即可创建另一个线程,两个线程并发运行
#include <iostream>
#include <thread>

void f() { std::cout << "hello world"; }

int main() {
  std::thread t{f};
  t.join();  // 等待新起的线程退出
}
  • std::thread 的参数也可以是函数对象或者 lambda
#include <iostream>
#include <thread>

struct A {//函数对象
  void operator()() const { std::cout << 1; }
};

int main() {
  /*函数对象*/
  A a;
  std::thread t1(a);  // (对象申明)会调用 A 的拷贝构造函数
  std::thread t2(A());  // (函数申明)most vexing parse,声明 名为 t2 、参数类型为 A 的 函数
  std::thread t3{A()};
  std::thread t4((A()));
    
  /*lambda*/
  std::thread t5{[] { std::cout << 1; }};
    
  t1.join();
  t3.join();
  t4.join();
  t5.join();
}

二、td::thread对象的等待与分离

下文中“底层线程”指std::thread对象关联的底层线程。比如创建std::thread th对象时创造的新线程为这个std::thread对象th的底层线程;而在执行th.detach()或th.join()之后,对象th将不与任何底层线程相关联

下文中“线程对象”指类型为std::thread的一个对象

(一)join和detach函数

对线程对象执行join或detach之后,都会和底层线程断开联系,joinable()都会变成返回false。其中执行join之后会释放底层线程的内存(所以这显然要断开线程对象和底层线程的联系);执行detach就是分离底层线程

  ### 1. 线程等待/联结:join()

  (1)等待子线程结束,调用线程处于阻塞模式

  (2)join()执行完成之后,底层线程id被设置为0,即joinable()变为false。同时会清理线程相关的存储部分, 这样 std::thread 对象将不再与底层线程有任何关联。这意味着,**只能对一个线程对象使用一次join()**。

 ### 2. 线程分离:detach()

  (1)分离子线程,与当前线程的连接被断开,子线程成为后台线程,被C++运行时库接管。 std::thread 对象将不再与底层线程有任何关联。与join一样,detach也只能调用一次,当detach以后其joinable()为false

  (2)注意事项:

    ①如果不等待线程,就必须保证线程结束之前,可访问的数据是有效的。**特别是要注意线程函数是否还持有一些局部变量的指针或引用(这些局部变量所在作用域在线程函数返回前可能已经结束了)**。

悬空引用问题:

#include <iostream>
#include <thread>

using namespace std;

class FuncObject
{
    void do_something(int& i) { cout <<"do something: " << i << endl; }
    
public:
    int& i; //注意:引用类型
    FuncObject(int& i) :i(i) {  } //注意:引用类型

    void operator()()
    {
        for (unsigned int j = 0; j < 1000; ++j)
        {
            do_something(i); //可能出现悬空引用的问题。
        }
    }
};

void oops()
{
    int localVar = 0;
    FuncObject fObj(localVar); //注意,fObj的成员i是引用局部变量localVar

    std::thread t1(fObj);

    t1.detach();  //主线程调用oops函数,可能出现oops函数执行完了,子线程还在运行的现象。它会去调用do_something,这时会访问到己经被释放的localVar变量,会出现未定义行为!如果这里改成join()则不会发生这种现象。因为主线程会等子线程执行完才退出oops
}

int main()
{
    oops();
    return 0;
}

    ②为防止上述的悬空指针和悬引用的问题,线程对象(std::thread对象)的生命期应尽量长于底层线程(std::thread对象创造时创造的线程)的生命期,或者将局部变量复制传入线程,而不是引用或指针

  (3)应用场合

    ①适合长时间运行的任务,如后台监视文件系统、对缓存进行清理、对数据结构进行优化等。

    ②线程被用于“发送即不管”(fire and forget)的任务,任务完成情况线程并不关心,即安排好任务之后就不管。

(二)联结状态:

线程对象的联结状态即std::thread对象是否与某个有效的底层线程关联,可用joinable()函数来判断,内部通过判断线程id是否为0来实现。一个std::thread对象只可能处于可联结或不可联结两种状态之一。

   1. **不可联结**:
      1. **己调用join或detach的std::thread对象**为不可联结状态。
      2. **不带参构造的std::thread对象**为不可联结,因为底层线程还没创建。
      3. **己移动的std::thread对象**为不可联结。因为该对象的底层线程id会被设置为0。
  1. 可联结:当线程可运行、己运行或处于阻塞时是可联结的 std::thread对象生命周期内的其他情况。注意,如果某个底层线程已经执行完任务但是没有被join的话,该线程依然会被认为是一个活动的执行线程,该std::thread对象仍然处于joinable状态

三、std::thread对象的析构

(一)std::thread的析构

  1. std::thread对象析构时,会先判断joinable(),如果可联结,则程序会直接被终止(std::thread对象的析构函数中会调用``std::terminate`函数)。

std::terminate 调用当前安装的 std::terminate_handler 。默认的 std::terminate_handler 调用 std::abort 这好像会终止进程

  1. 这意味std::thread对象从其它定义域出去的任何路径,都应为不可联结状态。也意味着创建thread对象以后,要在随后的某个地方显式地调用join或detach以便让std::thread处于不可联结状态。

(二)为什么析构函数中不隐式调用join或detach?

  1. 如果设计成隐式join():将导致调用线程一直等到子线程结束才返回。如果子线程正在运行一个耗时任务,这可能造成性能低下的问题,而且问题也不容易被发现。

  2. 如果设计成隐式detach():由于detach会将切断std::thread对象与底层线程之间的关联,两个线程从此各自独立运行。如果线程函数是按引用(或指针)方式捕捉的变量,在调用线程退出作用域后这些变量会变为无效,这容易掩盖错误也将使调试更加困难。因此隐式detach,还不如join或者显式调用detach更直观和安全。

  3. 标准委员会认为,销毁一个joinable线程的后果是十分可怕的,因此他们通过terminate程序来禁止这种行为。为了避免销毁一个joinable的线程,就得由程序员自己来确保std::thread对象从其定义的作用域出去的任何路径,都处于不可联结状态,最常用的方法就是资源获取即初始化技术(RAII,Resource Acquisition Is Initialization)。

(三)利用RAII技术:保证从std::thread对象定义的作用域出去的任何路径,都处于不可联结状态

异常抛出情况下

如果主线程(创造std::thread对象的线程)运行后在执行join()之前抛出异常,这样就会跳过join(),即这种情况下主线程可能会比它join的线程先结束,因为它中止在异常抛出的处理中了,而没有走到join,这种情况下主线程在结束的时候析构std::thread对象时,该对象是joinable的,则整个进程会终止

因此,如果在无异常的情况下要使用join(),则需要**在异常处理中也调用join()**,从而避免生命周期的问题。

struct func
{
  int& i; //注意这个是引用
  func(int& i_) : i(i_) {} //构造函数的参数是引用类型
  void operator() ()
  {
    for (unsigned j=0 ; j<1000000 ; ++j)
    {
      do_something(i);           // 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
}

代码2.2中使用了try/catch块确保(子)线程退出后(主线程的f)函数才结束。当函数正常退出后,会执行到②处。当执行过程中抛出异常,程序会执行到①处。如果线程在函数之前结束——就要查看是否因为线程函数使用了局部变量的引用——而后再确定一下程序可能会退出的途径,无论正常与否,有一个简单的机制,可以解决这个问题。

RAII方案

  1. 方案1:自定义的thread_guard类,并将std::thread对象传入其中,同时在构造时选择join或detach策略。当thread_guard对象析构时,会根据析构策略,**调用std::thread的join()或detach()**,确保在任何路径,线程对象都处于unjoinable状态。

  2. 方案2:重新封装std::thread类(见下面的代码,类名为joining_thread),在**析构时隐式调用join()**。

#include <iostream>
#include <thread>

class thread_guard
{
  std::thread t;
public:
  //构造函数
  explicit thread_guard(std::thread t_):
    t(std::move(t_))
  {}
  //析构函数
  ~thread_guard()
  {
    if(t.joinable()) 
    {
      t.join();      
    }
  }
  //拷贝语义函数删除
  thread_guard(thread_guard const&)=delete;   
  thread_guard& 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 g(std::move(t)); //调用thread_guard的构造函数,其中构造参数_t(_t是类型为std::thread的bian'lian)会调用std::thread的移动构造函数(把底层线程所有权给_t),然后t(std::move(_t))也会调用std::thread的移动构造函数(把底层线程所有权给t)
  do_something_in_current_thread();
  //主线程离开这里时,析构thread_guard g对象,调用其析构函数
}  

代码尚未阅读

//使用RAII等待线程完成:joining_thread类的实现
class joining_thread
{
    std::thread thr;
public:
    joining_thread() noexcept = default;

    //析构函数
    ~joining_thread()
    {
        if (joinable()) //对象析构造,会隐式调用join()
        {
            join();
        }
    }

    template<typename Callable, typename... Args>
    explicit joining_thread(Callable&& func, Args&& ...args):
        thr(std::forward<Callable>(func), std::forward<Args>(args)...)
    {
    }

    //类型转换构造函数
    explicit joining_thread(std::thread t) noexcept : thr(std::move(t))
    {
    }

    //移动操作
    joining_thread(joining_thread&& other) noexcept : thr(std::move(other.thr))
    {
    }

    joining_thread& operator=(joining_thread&& other) noexcept
    {
        if (joinable()) join(); //等待原线程执行完

        thr = std::move(other.thr); //将新线程移动到thr中

        return *this;
    }

    joining_thread& operator=(std::thread other) noexcept
    {
        if (joinable()) join();

        thr = std::move(other);

        return *this;
    }
    
    bool joinable() const noexcept
    {
        return thr.joinable();
    }

    void join() { thr.join(); }
    void detach() { thr.detach(); }

    void swap(joining_thread& other) noexcept { thr.swap(other.thr); }
    std::thread::id get_id() const noexcept { return thr.get_id(); }

    std::thread& asThread() noexcept //转化为std::thread对象
    {
        return thr;
    }
    const std::thread& asThread() const noexcept
    {
        return thr;
    }
};

void doWork(int i) { cout << i << endl; }

int main()
{
    //3. 测试joining_thread类
    std::vector<joining_thread> threads;  //joining_thread析构时隐式调用join
    for (unsigned int i = 0; i < 20; ++i) {
        threads.push_back(joining_thread(doWork, i));
    }

    std::for_each(threads.begin(), threads.end(), std::mem_fn(&joining_thread::join));

    return 0;
}

四、std::thread的移动语义

创建了两个执行线程,并在std::thread实例之间(t1,t2和t3)转移所有权:

void some_function();
void some_other_function();
std::thread t1(some_function);            // 1
std::thread t2=std::move(t1);            // 2 std::move返回的右值类型触发调用 std::thread的移动赋值重载函数,这个移动语义函数实现底层线程的“所有权”从t1移动到t2
//此时t1无关联线程
t1=std::thread(some_other_function);    // 3 临时std::thread对象启动了一个线程,临时对象是一个右值,因此触发调用 std::thread的移动赋值重载函数,将这个新触发的线程移动给t1
//现在t1和刚刚std::thread临时对象启动的线程相关联
std::thread t3;                            // 4 t3此时没有与任何线程进行关联
t3=std::move(t2);                        // 5
t1=std::move(t3);                        // 6 因为t1已经有了一个关联的线程,赋值操作将抛出异常,移动赋值重载函数是noexcept则异常处理机制将直接调用std::terminate()

函数返回std::thread对象:

std::thread f()
{
  void some_function();
  return std::thread(some_function); //构造返回值std::thread临时对象时是调用std::thread的移动构造函数
}

//这个编译器不会报错吗?试了一下不会
std::thread g()
{
  void some_other_function(int);
  std::thread t(some_other_function,42);
  return t; //构造返回值std::thread临时对象时是调用std::thread的移动构造函数
  //return std::move(t);比较符合我的li
}

std::thread实例作为参数进行传递:

void f(std::thread t);
void g()
{
  void some_function();
  f(std::thread(some_function));
  std::thread t(some_function);
  f(std::move(t));
}

评论