C++ 标准库 底层接口thread()、promise、packaged_task

原文链接:并发之(底层接口thread()、promise、packaged_task)

  • 除了前一篇文章介绍的高级接口async()和(shared)future,C++标准库还提供了一个启动及处理线程的底层接口

一、thread

thread概述

  • thread可以用来启动一个线程,其参数也接受一个callable object(函数、成员函数、函数对象、lambda)
  • callable object的传参方式与async()一样,并且也有传值调用和传引用调用的方式,详情可以参阅前一篇async()的文章
  • 例如:
std::thread t(doSomething);
 
//...
 
t.join();  //等待线程的结束

thread与async()的区别

  • 相比于async(),thread()不提供下面的性质:
    • thread没有所谓的发射策略C++标准库永远试着将目标函数启动于一个新的线程中。如果无法做到会抛出std::system_error并带有差错码resource_unavailable_try_agin
    • 没有接口可以处理线程结果。唯一可获得的是独一无二的线程ID
    • 如果发生异常,但为被捕捉于线程之内,程序会立刻终止并调用std::terminate()。如果想要将异常传播到线程外的某个context,必须使用exception_ptr
    • ④你必须声明是否“想要等待线程结束(调用join())”或打算“将它分离,使其运行于后台而不受任何控制(调用detach())”。如果你在thread object生命周期前不这么做,或如果它发生了一次move assignment,程序会终止并调用std::terminate()
    • ⑤如果你让线程运行于后台而main()结束了,所有线程会被鲁莽而硬性地终止

二、thread演示案例

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
 
void doSomething(int num, char c);
 
int main()
{
    try {
        //开启一个线程(不分离)
        std::thread t1(doSomething, 5, '.');
        std::cout << "- started fg thread " << t1.get_id() << std::endl;
 
        //开启5个线程(分离)
        for (int i = 0; i < 5; ++i) 
        {
            std::thread t(doSomething, 10, 'a' + i);
            std::cout << "-detach started bg thread " << t.get_id() << std::endl;
            t.detach();
        }
 
        //等待输入
        cin.get();
 
        //等待t1线程结束
        std::cout << "- join fg thread " << t1.get_id() << std::endl;
        t1.join();
    }
    catch (const exception& e) {
        std::cerr << "EXCEPTION: " << e.what() << std::endl;
    }
}
 
void doSomething(int num, char c)
{
    try {
        std::default_random_engine dre(42 * c);
        std::uniform_int_distribution id(10, 1000);
        for (int i = 0; i < num; ++i)
        {
            this_thread::sleep_for(std::chrono::milliseconds(id(dre)));
            std::cout.put(c).flush();
        }
    }
    catch (const exception& e) { //处理exception异常
        std::cerr << "THREAD-EXCEPTION (thread  " << this_thread::get_id() << "):" << e.what() << std::endl;
    }
    catch (...) { //捕获其他所有异常
        std::cerr << "THREAD-EXCEPTION (thread  " << this_thread::get_id() << ")" << std::endl;
    }
}
  • 我们在打印了“acd”之后按下回车,结果如下所示

C++ 标准库 底层接口thread()、promise、packaged_task_第1张图片

三、线程的分离(thread.detach())

  • detached thread(卸载/分离后的线程)很容易造成问题——线程分离之后就不再收到主程序的控制,因此你就无法判断其是否还在运行。因此如果detached thread访问非局部资源的话,或者以reference方式使用变量/object,要确保让detached thread运行的时候资源的生命周期没有结束
  • 如果当程序退出之后,detached thread可能还在运行,如果其仍然在访问“已被销毁”或“正在析构”的global/static object,这会导致不可预期的后果
  • 因此,对于detached thread,其建议使用规则如下:
    • detached thread应该访问局部对象的拷贝
    • 如果detached thread中使用了global/static object,你应该:
      • 确保这些global/static object在“对它们进行操作”的所有detached thread都结束(或都不再访问它们)之前不被销毁。一种做法是使用condition variable(条件变量),它让detached thread用来发信号说它们已结束。离开main()或调用exit()之前你必须先妥善设置这些condition variable,然后发信号说可进行析构了
      • 以调用quick_exit()的方式结束程序。这个函数之所以存在完全是为了以“不调用global和static object析构函数”的方式结束程序
  • 由于std::cin、std::cout、std::cerr及其global stream object按标准来说是“在程序运行期间不会被销毁”,所以detached thread访问这些object应该不会导致不可预期的行为。然而,其他问题例如“interleaved character”却有可能发生
  • 记住一个经验法则:终止detached thread的唯一安全方法就是搭配“...at_thread_exit()”函数群中的某一个。这会“强制main thread等待detached thread真正结束”。或者你也可以选择忽略这一性质而相信某位评论家所言:“Detached thread应该被移到'危险性质'的篇章中,几乎没有人需要它”

四、线程ID

  • this_thread::get_id()你可以根据this_thread命名空间来获取当前线程的ID,不需要通过线程对象获取
void doSomething();
 
int main()
{
    std::thread t(doSomething);
}
 
void doSomething()
{
    //打印线程ID
    std::cout << "thread id: " << this_thread::get_id() << std::endl;
}
  • object.get_id()根据一个线程对象,获取其线程id
void doSomething();
 
int main()
{
    std::thread t(doSomething);
    //打印线程ID
    std::cout << "t thread id: " << t.get_id() << std::endl;
}

std::thread::id数据类型

  • 线程ID的数据类型用std::thread::id表示,线程ID独一无二,其是一个类类型
int main()
{
    std::thread t(doSomething);
    //保存线程ID
    std::thread::id tThreadId = t.get_id();
    //打印ID
    std::cout << "t thread id: " << tThreadId << std::endl;
}
  • std::thread::id有个默认构造函数,会产生一个独一无二的ID用来表现“no thread”
void doSomething();
 
int main()
{
    std::thread t(doSomething);
    std::cout << "ID of \"no thread\":" << std::thread::id() << std::endl;
}

  • 线程ID的一些特性:
    • 线程ID支持的操作只有“比较”以及调用output操作符输出至某个stream。其他的操作不支持
    • 事实上,线程ID不是在thread启动时就生成的,而是在被使用时才生成的
void doSomething();
std::thread::id masterThreadID;
 
int main()
{
    std::thread master(doSomething);
    masterThreadID = master.get_id();
}
 
void doSomething()
{
    if (this_thread::get_id() == masterThreadID)
    {
        //...
    }
}

五、std::promise<>

  • 设计promise<>的目的:
    • 在async()中,我们可以将async()的结果(正确的返回值/或异常)保存在一个future<>中,然后使用future<>.get()去获取,但是在thread中我们如何获取线程中可能产生的数据或者是异常呢?
    • 标准库设计了一个promise<>,它是future<>的配对兄弟,二者配合使用,可以保存一个shared shate(用来保存结果或异常)
  • 演示案例
#include 
#include 
#include 
#include 
#include 
using namespace std;
 
void doSomething(std::promise& p);
 
int main()
{
    try {
        std::promise p;
        //将p以引用的方式传入doSomething中
        std::thread t(doSomething, std::ref(p));
        t.detach();
 
        //如果p有结果返回,将结果保存到f中
        std::future f(p.get_future());
        //调用.get()获取返回的结果
        std::cout << "result: " << f.get() << std::endl;
    }
    catch (const exception& e) {
        std::cerr << "EXCEPTION: " << e.what() << std::endl;
    }
    catch (...) {
        std::cerr << "EXCEPTION" << std::endl;
    }
}
 
void doSomething(std::promise& p)
{
    try {
        std::cout << "read char('x' for exceptipn):";
        char c = std::cin.get();
        if (c == 'x') {
            throw std::runtime_error(std::string("char ") + c + " read");
        }
 
        std::string s = std::string("char ") + c + " processed";
        //将字符串保存到p中返回
        p.set_value(std::move(s));
    }
    catch (...) {
        //将异常保存到p中返回
        p.set_exception(std::current_exception());
    }
}
  • 我们以引用的方式将promise<>传入到doSomething()函数中:
    • 如果有正确的数据,那么我们调用promise<>.set_value()将结果保存到promise<>中
    • 如果有异常,我们调用promise<>.set_exception()将异常保存到promise<>中。在此演示案例中我们将定义于内的辅助函数std::current_exception()传递给该函数,std::current_exception()会把当前异常以类型std::exception_ptr生成出来,如果没有异常,那么std::current_exception()生成nullptr
  • 与future<>的交互过程:
    • 我们可以将promise<>变量绑定到future<>上
    • 一旦promise<>调用set_...相关函数设置某个值或异常,那么shared state就存在该值或异常,此时shared state状态变为ready,于是future<>对象就可以调用get()获取shared state中promise<>返回的值或异常
    • future<>.get()会阻塞,直到shared state变为ready——有值或者异常了
  • 下面两张图是输入a和x的结果:

C++ 标准库 底层接口thread()、promise、packaged_task_第2张图片

C++ 标准库 底层接口thread()、promise、packaged_task_第3张图片

  • 如果想要shared state在线程结束时变成ready,以确保线程的局部对象以及其他资源在“结果被处理之前”清除,那么应该调用set_value_at_thread_exit()或set_exception_at_thread_exit()。例如:
void doSomething(std::promise& p)
{
    try {
        //..同上
        p.set_value_at_thread_exit(std::move(s));
    }
    catch (...) {
        p.set_exception_at_thread_exit(std::current_exception());
    }
}
  • 相关注意事项:
    • promise和future并不仅限于多线程中,在单线程中我们也可以使用promise持有一个结果值或一个异常,然后通过一个future进行处理
    • 我们不能够既存储值又存储异常。这么做会导致std::future_error并夹带差错std::future_errc::promise_already_staisfied

六、std::packaged_task<>

  • async()接口允许你处理一个任务(task)的结果,该task会自动运行于后台
  • 然而有时候处理一个task,不需要立刻启动该task

演示案例

  • 例如,thread poll(线程池)可控制何时运行以及多个后台task同时运行
  • 我们不应该像下面这样写:
double compute(int x, int y);
 
int main()
{
    std::future f = std::async(compute, 7, 5);
    double res = f.get();
}
  • 而应该使用packaged_task<>:
double compute(int x, int y);
 
int main()
{
    //创建一个task
    std::packaged_task task(compute);
    std::future f = task.get_future();
 
    //...执行其他事情
 
    //此时才执行该task
    task(7, 5);
 
    //...执行其他事情
 
    double res = f.get();
}

 

你可能感兴趣的:(C++标准库)