- 除了前一篇文章介绍的高级接口async()和(shared)future,C++标准库还提供了一个启动及处理线程的底层接口
一、thread
thread概述
- thread可以用来启动一个线程,其参数也接受一个callable object(函数、成员函数、函数对象、lambda)
- callable object的传参方式与async()一样,并且也有传值调用和传引用调用的方式,详情可以参阅前一篇async()的文章:https://blog.csdn.net/qq_41453285/article/details/105486757
- 例如:
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 <iostream>
#include <thread>
#include <future>
#include <random>
#include <chrono>
#include <exception>
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<int> 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”之后按下回车,结果如下所示
三、线程的分离(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 <iostream> #include <thread> #include <future> #include <string> #include <exception> using namespace std; void doSomething(std::promise<std::string>& p); int main() { try { std::promise<std::string> p; //将p以引用的方式传入doSomething中 std::thread t(doSomething, std::ref(p)); t.detach(); //如果p有结果返回,将结果保存到f中 std::future<std::string> 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<std::string>& 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<>中。在此演示案例中我们将定义于<exception>内的辅助函数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的结果:
- 如果想要shared state在线程结束时变成ready,以确保线程的局部对象以及其他资源在“结果被处理之前”清除,那么应该调用set_value_at_thread_exit()或set_exception_at_thread_exit()。例如:
void doSomething(std::promise<std::string>& 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<double> f = std::async(compute, 7, 5); double res = f.get(); }
- 而应该使用packaged_task<>:
double compute(int x, int y); int main() { //创建一个task std::packaged_task<double(int, int)> task(compute); std::future<double> f = task.get_future(); //...执行其他事情 //此时才执行该task task(7, 5); //...执行其他事情 double res = f.get(); }