Marvin's Blog【程式人生】

Ability will never catch up with the demand for it

12 Nov 2018

c++11的线程(threads)

本文讨论C++11中新引入的对线程的支持。

std::thread

C++在11标准中增加了对线程(threads)的支持,看以下例子:

// t1.cpp
#include <thread>
#include <iostream>
using namespace std;

void f() {
  cout << "hello, world!" << endl;
}

int main() {
  thread t{f};
  t.join();
  return 0;
}

编译:把上面的代码保存为t1.cpp,并执行g++ -std=c++11 t1.cpp -o t1,既可以生成可执行文件t1。执行t1可以得到结果:hello, world!

使用C++11的线程功能必须包含<thread>头文件,之后便可以使用std::thread类来创建一个线程。创建线程的时候必须传入一个可执行体作为参数,在上面的例子中这个可执行体是函数f()

std::promise

为了在不同的线程之间传递数据,C++引入了std::promise和std::future这两种数据结构,在头文件<future>中包含。

promise是一个范型的数据结构,你可以定义一个整形的promise:promise<int>,这意味着线程之间传递的值是整形。promise的get_future()方法返回一个future数据结构,从这个future数据结构可以获取设置给promise的值,下面是一个例子:

#include <iostream>
#include <future>
#include <thread>
using namespace std;

int main() {
  promise<int> a_promise;
  auto a_future = a_promise.get_future();
  a_promise.set_value(10);
  cout << a_future.get() << endl;
  cout << "after get()" << endl;
  return 0;
}

上面例子的输出结构是:

10
after get()

实际上,上面的例子并没有使用线程,但是很好得展示了promise和future之间的关系。 更复杂一点的使用场景可能是下面这样子的:

  • 主线程定义一个promise,命名为p
  • 主线程调用p.get_future(),并把返回值保存为引用f
  • 主线程启动一个子线程,并把p作为启动参数传给子线程
  • 主线程调用f.get(),但是此时子线程还未将数据放入promise内,所以主线程挂起
  • 子线程执行完,获取到结果,并把结果写入p
  • 主线程从f.get()的调用中被唤醒,获取到子线程写入p的值,继续执行

std::packaged_task

C++11很贴心地提供了packaged_task类型,让我们不用直接使用std::thread和std::promise,直接就能够生成线程,派遣任务:

#include <iostream>
#include <future>
using namespace std;

int f() {
  string hi = "hello, world!";
  cout << hi << endl;
  return hi.size();
}

int main() {
  packaged_task<int ()> task(f);
  auto result = task.get_future();

  task();

  cout << result.get() << endl;

  return 0;
}

上面代码的运行结果为:

hello, world!
13

std::async

std::packaged_task要求你自己启动任务,比如上一章节例子中你要显示调用task()。如果连这一步都想省了的话,可以使用std:async:

#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
#include <future>
 
template <typename RandomIt>
int parallel_sum(RandomIt beg, RandomIt end)
{
    auto len = end - beg;
    if (len < 1000) 
        return std::accumulate(beg, end, 0);
 
    RandomIt mid = beg + len/2;
    auto handle = std::async(std::launch::async,
                             parallel_sum<RandomIt>, mid, end);
    int sum = parallel_sum(beg, mid);
    return sum + handle.get();
}
 
int main()
{
    std::vector<int> v(10000, 1);
    std::cout << "The sum is " << parallel_sum(v.begin(), v.end()) << '\n';
}

上面的代码来自 cpp reference,运行结果:The sum is 10000

std::this_thread

C++11专门提供了一个命名空间std::this_thread来表示当前线程。std::this_thread提供了几个方法可以对线程做一定的控制:

  • get_id(),获取线程id
  • yield(), 释放执行权
  • sleep_for(),使线程沉睡一定时间

下面是一个具体例子:

#include <iostream>
#include <future>
#include <thread>

using namespace std;

int f(promise<int> my_promise) {
  string hi = "hello, world!";

  my_promise.set_value(hi.size());

  this_thread::sleep_for(0.1s);
  cout << hi << endl;
}

int main() {
  promise<int> f_promise;

  auto result = f_promise.get_future();

  thread f_thread(f, move(f_promise));
  cout << result.get() << endl;

  f_thread.join();

  return 0;
}

程序退出相关的函数

由于增加了线程,如果优雅得退出一个程序变得更加复杂,因为当主线程决定要退出的时候,它并不知道其他线程正在干什么。

看看下面这些程序退出相关的函数:

  • abort(),异常中止程序,不承诺清理资源,不调用atexit()注册的清理函数。实际上程序会接收到信号SIGABRT,并退出。
  • terminate(), 程序中抛出异常,但是没有得到处理,于是调用这个函数来终止程序,实际的实现会调用abort()
  • exit(),正常退出整个程序(包括所有线程),清理所有资源,并调用atexit()注册的清理函数。对于C++程序而言,会调用静态对象的析构函数,并处理好IO缓冲。
  • quick_exit(), 正常退出整个程序,会处理好IO缓冲,但是不会调用静态对象的析构函数,并且会调用at_quick_exit()注册的清理函数
  • _exit()/_Exit(),正常退出程序,但是不调用atexit()注册的清理函数,不清理任何资源

总结下问题:如果想使用exit()来从多线程函数中退出,必须等所有的线程都执行完。如果不进行这个同步的话,exit()执行的时候会对析构静态对象,而哪些在调用exit()之后才运行结束的线程也会析构静态对象,会导致对静态对象的二次析构,从而导致未定义的结果。另外一个选项是直接调用_exit()/_Exit()(这两等价)来直接终止程序,但是这样做完全不会清理任何资源,可能会导致重要的资源没有被释放。所以C++11中引入了quick_exit()函数,不对静态对象进行析构,但是可以通过at_quick_exit()注册清理函数,用来清理和释放重要的资源。

Abandoning a Process

参考链接

系列教程

书籍

Categories

Tags