C++并发编程实践笔记(二)——管理线程

0. std::thread 介绍

摘自:std::thread
std::thread 对象可以不关联任何线程,线程也可以不与 std::thread 对象关联(调用 detach 后)。
没有两个 std::thread 对象可以表示同一个线程,std::thread 不可复制构造,也不可以复制赋值,但是可以移动构造和移动赋值。
这里写图片描述

1.基本管理线程

1.1启动线程

启动一个线程,需要给 std::thread 对象传递一个可调用(callable) 的对象。
(1).全局函数或是静态成员函数
这个很好理解,就是把普通的全局函数或者静态成员函数传给 std::thread 对象。

void TestStaticFunc() {
    std::cout << "TestStaticFunc" << std::endl;
    std::thread td(&TestThread::PrintClassName);
    if (td.joinable()) {
      td.join();
    }
}

(2)类的非静态成员函数

void TestMemFunc() {
    std::cout << "TestMemFunc" << std::endl;
    TestThread obj(2);
    std::thread td(&TestThread::Print, &obj);
    if (td.joinable()) {
      td.join();
    }
}

(3)函数对象

class FuncTask {
  public:
    void operator() () const {
      std::cout << "background_task" << std::endl;
    }
};
void TestFunctor() {
    std::cout << "TestFunctor" << std::endl;
    FuncTask  ftor;
    std::thread td(ftor);
    if (td.joinable()) {
      td.join();
    }
}

(4)lambda 表达式

void TestLambda() {
    std::cout << "TestLambda" << std::endl;
    std::thread td([] {
      std::cout << "Lambda" << std::endl;
    });
    if (td.joinable()) {
      td.join();
    }
  }

1.2 join 或 detach 线程

一旦开始了线程,就需要显示的决定是等待它完成,还是让它独自运行。如果你在 std::thread 对象被销毁前还没有做决定,那么这个线程将被终止(std::thread 对象的析构函数调用 std::terminate())。
需要注意的是,只需要在 std::thread 对象被销毁之前做这个决定即可,线程本身可能在你结合或者分离之前就早已经结束了,而且,如果你决定分离它,该线程可能在 std::thread 对象被销毁后很久都还在运行。
如果你不等待线程完成,那么你需要确保线程访问的数据都是有效的,直到该线程结束。
当线程持有局部变量的指针或者引用,且当函数退出的时候线程尚未完成,那么就有可能出现引用已经被销毁的对象。

void DoSomething(const int & num) {
    for (int i = 0; i < num; ++i) {
      std::cout << i << " ";
    }
  }
  void Test() {
    int num = 1000000;
    std::thread td(DoSomething, num);
    if (td.joinable()) {
      td.detach();
    }
    std::cout << "main thread end." << std::endl;
  }

上面这段代码描述的就是这种情况,Test() 函数早已经结束了,而 DoSomething() 还在引用临时变量 num。在测试的时候,DoSomething() 并没有被强制终止,但是这么写确实很危险。
针对这种现象,一种解决办法是给线程传递值,而不是引用;另一种解决办法就是 join 该线程,直到它结束。
调用 join 的行为,将清理所有与该线程相关联的存储器,这样 std::thread 对象不再与现有已完成的线程相关联。
只能对一个线程执行一次 join,一旦对一个线程调用了 join(),此 std::thread 对象不再是可连接的,并且 joinable() 将返回 false。

1.3 在异常情况下等待

如前所述,你需要在 std::thread 对被销毁之前调用 join() 或 detach() 。如果分离线程,通常在线程启动后就可以立即调用 detach() 。但是如果你决定 join 一个线程,就要仔细的选择在代码的哪个位置调用 join() 。这意味着,如果在线程开始之后但又在调用 join 之前发生异常,join 方法将不会被调用。
在 C++ 中,一种方法是可以通过 try/catch 来保证异常情况下 join 被调用,但是这种方案比较啰嗦,需要在 try 和 catch 两个地方调用 join。
另一种方法是资源获取即初始化,代码如下:

class ThreadGuard {
  public:
    explicit ThreadGuard(std::thread &td)
      :td_(td) {

    }

    ~ThreadGuard() {
      if (td_.joinable()) {
        td_.join();
      }
    }

    ThreadGuard(const ThreadGuard &) = delete;
    ThreadGuard& operator= (const ThreadGuard&) = delete;
  private:
    std::thread & td_;
};

void Test() {
    int num = 1000;
    std::thread td(DoSomething, num);
    ThreadGuard guard(td);
    if (td.joinable()) {
      td.detach();
    }
    std::cout << "main thread end." << std::endl;
}

1.4 在后台运行线程

在 std::thread 对象上调用 detach() 会把线程丢在后台运行,也没有直接的方法与之通信。分离的线程确实在后台运行,所有权和控制权被转交给 C++ 运行时库,以确保和线程相关的资源在线程退出后能够被正确地回收。
参考 UNIX 守护进程的概念(daemon process),被分离的线程通常被称为守护线程,它们无需任何用户界面,运行在后台。这样的线程通常是长时间运行的,它们可能在应用程序的整个生命周期中都在运行,执行后台任务,例如监控文件系统,清除对象缓存中的未使用项。
只能对 joinable() 的线程执行 detach 或者 join。

2.给线程传递参数

2.1 传递临时对象给线程

传递临时对象给线程对象,代码如下:

void Func(const std::string & str) {
    for (int i = 0; i < 1000000; ++i) {
      ;
    }
    std::cout << str << std::endl;
  }

  void TestTemp() {
    char buffer[1024] = "hello, qing.";
    std::thread td(Func, buffer);
    if (td.joinable()) {
      //td.detach();
    }
    std::cout << "main thread end." << std::endl;
  }

根据观察到的现象,如果执行 td.detach() ,程序能够正常运行,但是如果注释这一句,代码在运行结束后会抛出异常。
这里写图片描述
即使按照《C++并发编程实践》中说的,用 std::string 来包装 buffer,如果没有 td.detach() 执行,程序运行结束时仍然会抛出异常。

std::thread td(Func, std::string(buffer));

由此可见,在创建一个线程时,要么 join 要么 detach ,如果没有选择这两者之一,将导致程序处于危险的境地。

2.2 传递引用给线程

按照书上说的,如果线程入口函数要求的是引用,但是你传递的是值,只会导致最终结果不正确,程序还是会正常执行。遗憾的是,我实践的结果却不同,代码如下:

void FuncRef(int & num) {
    num = num * 2;
  }

void TestRef() {
    int num = 12;
    //std::thread td(FuncRef, std::ref(num));
    std::thread td(FuncRef, num);
    if (td.joinable()) {
      td.join();
    }
    std::cout << "num: " << num << std::endl;
}

如果向线程入口函数传递的是 num 而非 std::ref(num),程序编译就报错了,根本就无法正常运行,但是改为 std::ref(num)后运行正常。
这里写图片描述

2.3 传递 unique_ptr 给线程

void FuncUniquePtr(std::unique_ptr<int> num) {
    std::cout << *num << std::endl;
  }

  void TestUnique() {
    std::unique_ptr<int> ptr(new int(2));
    std::thread td(FuncUniquePtr, std::move(ptr));
    if (td.joinable()) {
      td.join();
    }
  }

3.转移线程的所有权

假设你想要编写一个函数,它创建一个后台线程,但是向调用函数传回新线程的所有权,而非等待它完成。又或者反过来,创建一个线程,并将所有权传递给要等待它完成的函数。在这些情况下,都需要将所有权从一个地方转移到另一个地方。
在 C++ 标准库中许多拥有资源的类型,如 std::ifstream 和 std::unique_ptr 都是可移动的,但是不可以复制,并且 std::thread 就属于其中之一。这意味着一个特定线程的所有权可以在 std::thread 之间移动。

3.1 std::move 移动线程对象

void Func() {
    for (int i = 0; i < 1000000; ++i) {
      int a = 1;
      a * a;
    }
  }

  void TestMove() {
    std::thread td(Func);
    std::thread td1;
    if (td1.joinable()) {
      std::cout << "td1 is joinable." << std::endl;
      td1.join();
    }

    std::thread td2(std::move(td));
    if (td.joinable()) {
      std::cout << "td is joinable." << std::endl;
      td.join();
    }
    if (td2.joinable()) {
      std::cout << "td2 is joinable." << std::endl;
      td2.join();
    }
  }

上述代码的运行结果,只有 td2 是可以 join 的。
这里写图片描述
线程对象 td1 是不可 join的,td 因为将线程的所有权转移给 td2 导致自身不可 join,但是 td2 变得可 join。

3.2 线程对象作为函数参数返回值

void DoSomething() {
    std::cout << "Hello, qing." << std::endl;
  }

  std::thread Func() {
    return std::thread(DoSomething);
  }

  std::thread FuncTemp() {
    std::thread td(DoSomething);
    return td;
  }

  void FuncThreadParam(std::thread td) {
    if (td.joinable()) {
      td.join();
    }
  }

  void Test() {
    std::thread td = FuncTemp();
    if (td.joinable()) {
      td.join();
    }

    FuncThreadParam(std::thread(DoSomething));
  }

3.3 自动析构的线程对象

将线程对象传入一个类中,在类析构的时候等待线程执行完毕。

  class ScopedThread {
  public:
    explicit ScopedThread(std::thread td)
      : td_(std::move(td)) {
      if (!td_.joinable()) {
        throw std::logic_error("No thread.");
      }
    }

    ~ScopedThread() {
      td_.join();
    }

    ScopedThread(const ScopedThread&) = delete;
    ScopedThread& operator=(const ScopedThread&) = delete;
  private:
    std::thread td_;
  };

  void ScopedFunc() {
    ScopedThread scoped_thread({ std::thread(DoSomething) });
  }

3.4 容器与线程对象

  void DoSomething() {
    std::cout << "Hello, qing." << std::endl;
  }

  void Func() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
      threads.push_back(std::thread(DoSomething));
    }

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

  void Test() {
    Func();
  }

4.选择线程的数量

C++标准提供对选择线程数量的特性是 std::thread::hardware_currency()。这个函数给出程序执行时能够真正并发运行的线程数量的指示。例如,在多核系统上它可能返回 CPU 核心的数量,如果该信息不可用会返回0.

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

  template<typename Iterator, typename T>
  T ParallelAccumulate(Iterator first, Iterator last, T init) {
    unsigned long const length = std::distance(first, last);

    if (!length) {
      return init;
    }

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

    unsigned long const hardware_threads = std::thread::hardware_concurrency();
    std::cout << "hardware_threads: " << hardware_threads << std::endl;
    unsigned long const num_threads = std::min(hardware_threads ? hardware_threads : 2, max_threads);
    std::cout << "num_threads: " << num_threads << std::endl;

    unsigned long const block_size = length / num_threads;

    std::vector<T> results(num_threads);
    std::vector<std::thread> threads(num_threads - 1);

    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);
      threads[i] = std::thread(accumulate_block<Iterator, T>(), 
        block_start, block_end, std::ref(results[i]));
      block_start = block_end;
    }
    accumulate_block<Iterator, T>()(block_start, last, results[num_threads - 1]);

    std::for_each(threads.begin(), threads.end(), std::mem_fn(&std::thread::join));
    return std::accumulate(results.begin(), results.end(), init);
  }

  void Test() {
    std::vector<int> vec;
    for (int i = 0; i < 100000; ++i) {
      vec.push_back(i);
    }
    int ans = ParallelAccumulate(vec.begin(), vec.end(), 0);
    std::cout << ans << std::endl;
  }

实际创建线程的数量是期望的线程数与硬件能够提供的线程数的最小值。
从代码中会看到,创建的 std::thread 对象比实际的线程数量少一个(num_threads - 1),这是因为可以利用主线程来做计算,加上主线程的数量刚好是需要的线程数量。

5.标识线程

线程标识符是 std::thread::id 类型的, 有两种获取方式:

  void DoSomething(const int & num) {
    std::cout << "thread id: " << std::this_thread::get_id() << std::endl;
    for (int i = 0; i < num; ++i) {
      std::cout << i << " ";
    }
  }

  void Test() {
    int num = 10;
    std::thread td(DoSomething, num);
    std::cout << "thread id: " << td.get_id() << std::endl;

    if (td.joinable()) {
      td.detach();
    }
    std::cout << "main thread end." << std::endl;
  }

代码运行结果如图:
这里写图片描述

如果 std::thread 对象没有关联执行线程,get_id() 返回一个默认构造的 std::thread::id 对象,表示没有线程。
std::thread::id 类型的对象可以自由的复制和比较,如果两个 std::thread::id 相等,则它们代表同一个线程,或者两个具有“没有线程”的值。如果两个 std::thread::id 对象的值不相等,则它们代表不同的线程,或者一个代表线程,另一个具有“没有线程”的值。

线程库不限制检查线程的标识符是否相同,std::thread::id 类型的对象提供了一套完整的比较运算符,提供了所有不同值的总排序。这允许它们在关系型容器中被用作主键,或是被排序。

此外,当前线程的 std::thread::id 可以作为操作的一部分而存储在数据结构中,以后在相同数据结构上的操作可以对照此操作的线程 id 来检查所存储的 id 来确定哪些操作是允许的。可以用于限定某些操作只能在指定线程上进行,chromium 源码中有很多这种用法,将某些操作限定在 UI 或者是 FILE 线程。

6.小结

这一章介绍了:启动线程、等待线程、给线程传递参数、转移线程所有权、以及返回线程对象等,最后介绍了线程标识符。到目前为止,我们可以利用线程在独立数据集上做很多事情,但是还无法驾驭多个线程共享数据集的场景,接下来的章节会介绍如何实现在多个线程之间安全的共享数据集。

猜你喜欢

转载自blog.csdn.net/zhuiyuanqingya/article/details/81814515
今日推荐