一、C++线程库概述
- 在C++11之前,不论语言或标准库,对并发处理没有任何支持
- 在C++11之后,不论语言还是标准库都支持并发编程
- 语言核心定义了一个内存模型,保证当你更改“被两个不同线程使用”的两个object时,它们彼此独立,并引入了一个新关键字thread_local用以定义“变量带有thread专属值”
- 标准库提供的支持允许你启动多线程,包括得以传递实参、返回数值、跨线程边界传递异常、同步化等,使我们能够“控制流程”和“数据访问”实现同步化
- 高级接口、低级接口:
- 标准库提供了一个高级接口async(),允许你启动线程,包括传递实参、处理结果和异常。高级接口是在底层接口上封装实现的
- 标准库还提供了一组底层接口,例如mutex、atomic等
二、高级接口:async()、future<>、shared_future<>
- async():提供一个接口,让一段机能或说一个callable object(可调用对象)若是可能的话在后台运行,称为一个独立线程
- class future<>:允许你等待线程结束并获取其结果(一个返回值,或者也许是一个异常)
- class shared_future<>:与future类似,详细在文章最后介绍
async()的注意事项
- async()执行到之后,会立刻异步启动于一个分离线程内,并不会阻塞调用其的那个线程/进程
- 参数的注意事项:
- 传入的callable object就可以是函数、成员函数、函数对象或lambda
- 例如,你可以采用inline形式将“应该在专属线程中运行”的函数写成一个lambda并传递之:
std::async([] {...});
- 调用async()之后并不保证传入的函数会被立即执行,因此还会有以下几种情况:
- ①如果当前有线程可以使用,那么就启动线程开始执行async()所绑定的函数
- ②如果当前没有线程可用/或者不支持多线程,那么async()所绑定的函数不会被执行,直到发生以下的情况明确需要获取其结果时,async()所绑定的函数才会被强制执行:
- 调用futrue<>.get()获取其结果
- 或者希望目标函数完成其任务(调用futrue<>.wait())
- 如果async()没有被绑定到一个future<>上,那么async()永远不会被执行的情况:
- 通过上面我们知道,如果当前不支持,多线程或者没有线程可以使用,那么async()就不会执行,但是在调用futrue<>.get()或者wait()时会强制执行
- 如果你的系统不支持多线程,也没有线程可以使用,并且没有将其结果绑定到future<>上,那么你这个async()就永远不会执行,即使main()函数执行结束之后也不会被运行(因为get()或wait()会强制启动async()执行,但是此处不会发生)
- 当然,如果系统支持多线程,并且有线程可用,async()也执行起来了,那么也不需要将其返回值绑定到future<>上
- 如果async()被绑定到一个future<>上,作用域结束之后:
- 如果async()执行结束,那么程序正常结束(不论future<>有没有调用get()或wait()函数)
- 如果async()还没有执行结束,并且future<>没有调用任何get()或wait()函数,那么future<>的析构函数会阻塞等待,直至async()所绑定的函数执行结束之后才会继续运用
- 返回值的注意事项:
- async()的返回值可以是正常返回的结果,也可能是返回的一个异常
- async()的返回值可以用class future<>保存,其返回值与async()调用的callable object的返回值,因此定义class future<>时,其传入的数据类型要与async()调用的callable object的返回值一致
- 也可以定义一个auto变量作为async()的返回值,那么auto会被自动推导为future<>类型
- 如果async()调用的callable object无返回值,那么可以定义一个future<void>进行保存,其是future<>的一个偏特化版本
三、future<>.get()
future<>.get()接口介绍
- 当我们使用future<>保存async()的结果之后,可以调用future<>.get()获取async()的callable object的返回结果
- get()的接口有以下三种形式:
- 形式①:以拷贝的形式,获取async()执行的callable object的返回值
- 形式②:以引用的方式,获取async()执行的callable object的返回值
- 形式③:async()执行的callable object无返回值,那么不需要用取得future<>.get()的内容
- 调用future<>.get()有3种情况:
- ①如果其所绑定的async()已经执行结束,那么直接获取其返回值
- ②如果其所绑定的async()还在执行当中,那么阻塞等待async()的结束
- ③如果其所绑定的async()还未执行,那么启动async()开始执行(此处是阻塞的,要等到async()结束之后才可以继续运行,与async()的默认行为不同)
- 注意事项:
- 如果async()所绑定的callable object有返回值,那么future<>.get()会返回其结果
- 如果async()所绑定的callable object无返回值,那么future<>.get()也不会返回任何内容,单单就是为了获取结果/等待async()的执行结束
需求
- 现在我们需要计算两个操作数的和,每个操作数都有一个函数返回
- 如果采用单线程,那么格式如下,这两个函数会按照次序执行(先执行func1()再执行func2()),那么两个函数的执行时间等于每个函数各自执行时间的综合
//得到两个操作数的和 func1() + func2()
- 现在我们使用async(),使两个函数并行运行计算结果,那么两个函数的执行时间将为“两个函数运行时间中的较大者”
编码实现
- 先调用async()启动线程(非阻塞),让其运行func1(),然后接着运行fun2(),最后计算两个函数的结果(注意futrue<>.get()的用法)
- 在func1()和func2()中我们让其都运行doSomething(),doSomething()函数是为了打印一些字符,来显示两个线程的执行情况,在其中运用了随机数进行休眠
#include <iostream> #include <thread> #include <future> #include <random> #include <chrono> using namespace std; int doSomething(char c); int func1(); int func2(); int main() { std::cout << "starting func1() in background and func2() int foreground" << std::endl; std::future<int> result1(std::async(func1)); int result2 = func2(); //注意get()的用法 int result = result1.get() + result2; std::cout << "\nresult of func1()+func2(): " << result << std::endl; } int doSomething(char c) { //我们将独一无二的seed传递给“随机数生成器”构造函数,确保产出不同的随机数序列 std::default_random_engine dre(c); std::uniform_int_distribution<int> id(10, 1000); for (int i = 0; i < 10; ++i) { //休眠指定的秒数,秒数使用随机数 this_thread::sleep_for(std::chrono::milliseconds(id(dre))); //输出字符 cout.put(c).flush(); } return c; } int func1() { return doSomething('.'); } int func2() { return doSomething('+'); }
- 结果如下:
- 如果当前编译器不支持多线程/或者当前程序没有线程可以使用,那么结果会如下面所示:
- async()不会被执行,直到调用futrue<>.get()时,async()所绑定的函数才会被执行
“早调用,晚返回”建议
- 现在我们把上面的代码改为下面的样子
std::future<int> result1(std::async(func1)); int result = func2() + result1.get();
- 通过async()的注意事项我们知道,async()所绑定的函数可能不一定会被立即执行,如果当调用future<>.get()时,async()的线程才执行,那么这个程序就还是没有达到并行运行的效果,依然是按顺序执行
- 为了获得最佳效果,一般而言,应该将async()的调用和future<>.get()的调用之间的距离最大化,也就是“早调用而晚返回”
四、Launch(发射)策略
- 在创建async()时,可以给async()一些策略
- async()一共提供了两个策略:
- std::launch::async
- std::launch::deferred
绝不推延目标函数执行策略(std::launch::async)
- 通过上面的async()注意事项我们可以知道,async()并不总是立即执行(与环境有关),因此其锁绑定的函数可能并不会立即被执行
- 但是我们可以在创建async()时,给其指定“绝不推延目标函数执行”策略,让其立即以异步方式启动所绑定的目标函数。例如:
//async()所绑定的函数会被立即异步执行,而不是由于其他原因延缓/阻塞执行 auto result = std::async(std::launch::async, func1);
- 有了这个策略,程序就不必非得要调用future<>.get()了,因为一旦指定了这个策略,并且函数被成功执行,那么程序(main())就一定会等待其所运行的函数结束。但是使用future<>.get(),其会是程序行为更加清晰
- 如果不将其结果保存,那么线程会变为同步调用:
- 如果指定了这个策略,但是没有将async()的结果保存在furure<>中,那么程序会阻塞在async()调用处,直到函数执行结束
- 例如:
//没有将async()的结果用一个future<>保存,那么这个函数将会被同步执行(程序阻塞在此处等待func1()的执行完) std::async(std::launch::async, func1);
- 异常处理:如果指定了策略,但是由于种种原因,函数无法立即异步执行,那么程序会抛出一个std::system_error,差错码为resource_unavailable_try_again(其相当于POSIX的errno EAGAIN)
强制延缓执行策略(std::launch::deferred)
- 你可以在创建async()时,给其指定一个“强制延缓执行策略”,让其所绑定的函数绝不会被执行,直到等到调用future<>.get()时,async()所绑定的函数才会被执行
- 例如:
//async绝不会启动线程执行func1() std::future<int> result1(std::async(std::launch::deferred,func1)); int result2 = func2(); //直到调用future<>.get()时func1()才会被执行 int result = result1.get() + result2;
- 这个策略的特别之处在于,允许你写出lazy evaluation(缓式求值)。例如:
auto f1 = std::async(std::launch::deferred, task1); auto f2 = std::async(std::launch::deferred, task2); //... auto val = thisOrThatIsTheCase() ? f1.get() : f2.get();
- 此外,明确申请deferred发生策略也许有助于在一个单线程环境中模拟async()的行为,或者简化调试——除非需要考虑race condition(竞争形势)
五、async()的异常处理
- 规则如下:
- async()所绑定的函数如果正常执行结束,那么其结果可以保存在future<>中
- async()所绑定的函数如果抛出异常,那么future<>会保存其异常状态,在调用future<>.get()时,将异常从async()线程传播到外部进行处理
演示案例
- 下面开启一个后台任务,带有一个无线循环,为其分配内存,然后为list插入元素。这个程序迟早会抛出异常(可能是个bad_alloc异常),该异常会终止线程,因为它未被捕获。future一直保持这个状态,当调用future<>.get()时这个异常被传播到主进程中被处理
- 备注:下面这个程序运行要小心,因为程序一直在消耗内存,我在Windows下测试时,系统卡死
#include <iostream> #include <thread> #include <future> #include <exception> #include <list> using namespace std; void task1(); int main() { std::cout << "starting 2 tasks" << std::endl; std::cout << "- task1 process endless loop of memory consumption" << std::endl; std::cout << "- task2 wait for <return> and then for task1" << std::endl; auto f1 = async(task1); cin.get(); std::cout << "\nwait for the end of task1" << std::endl; try { f1.get(); } catch (const exception& e) { std::cerr << "EXCEPTION: " << e.what() << std::endl; } } void task1() { std::list<int> v; while (true) { for (int i = 0; i < 1000000; ++i) v.push_back(i); cout.put('.').flush(); } }
六、future<>.valid()
- 一个future<>只能被get()调用一次,调用之后future<>就处于无效状态,处于无效状态之后,对future<>的任何调用(除析构函数之外)会导致不可预期的行为(详情见后面“细说启动线程”文章)
- 对于future<>的状态,可以使用valid()来检测,其返回值为bool类型
七、等待和轮询
- 除了get()之外,future<>还提供了一些其他接口,用来等待后台操作完成而不需要处理其结果。这些接口可以被调用一次以上
①wait()
- 调用此函数时:
- 如果async()已经结束完,那么就什么都不做
- 如果async()线程已经启动,但还没有执行结束,那么会等待async()线程结束
- 如果async()线程没有启动,那么就强制启动async()线程执行并等待线程执行结束
- 例如:
auto f(std::async(func)); //... f.wait(); //等待async()线程执行完才返回
②wait_for()
- wait_for()特点:
- 也是用来等待async()线程
- 但是如果async()线程还没有启动,其不会强制启动async()线程,而只是单单的等待一段有限时间
- 返回值为std::future_status类型
- 其参数为一个时间段
- 注意事项:在一种情况下,wait_for()即使设置了时间也不会等待(见下面返回值介绍的①)
- 例如:
auto f(std::async(func)); //... //不论async()线程有没有执行,其阻塞等待10秒(并不会强制启动async()线程) f.wait_for(std::chrono::seconds(10));
③wait_until()
- 与wait_for()一样,也用来等待线程,不会强制启动线程,其参数可以等待直至达到某特点时间点
- 返回值为std::future_status类型
- 注意事项:在一种情况下,wait_for()即使设置了时间也不会等待(见下面返回值介绍的①)
- 例如:
auto f(std::async(func)); //... //不论async()线程有没有执行,其阻塞到当前时间的1分钟之后返回 f.wait_until(std::chrono::system_clock::now() + std::chrono::minutes(1));
wait_for()、wait_until()的返回值
- 这两个函数会返回以下三种东西之一:
- ①std::future_status::deferred——如果async()延缓了操作而程序中又完全没有调用wait()或get()。这种情况下wait_for()/wait_until()根本就不等待,而是直接返回(因为其async()线程根本就没有启动,等待也是白费)
- ②std::future_status::timeout——wait_for()/wait_until()结束之后,async()的线程还未执行完(注意,这种情况是async()没有延缓操作的情况,其线程执行了,只是还没执行完而已,与①不同,①是根本没有执行)
- ③std::future_status::ready——wait_for()/wait_until()结束之后,async()也已经完全执行结束
检测后台任务是否启动/是否正在运行
- 如果对wait_for()或wait_until()传入一个0时间段,或者一个过去时间点,那么wait_for()或wait_until()就会立刻返回,然后利用其返回值来检测async()的后台任务是否启动/是否正在运行
- 例如:
auto f(std::async(task)); //... //wait_for直接返回,然后根据返回结果判断async()的后台任务是否已经运行结束,如果没有,那么继续执行while while (f.wait_for(std::chrono::seconds(0)) != future_status::ready) { //... }
- 备注:上面这个while循环可能永远无法结束,因为在单线程环境中,这一调用将被推迟至future<>.get()的调用
- 因此,如果你调用async()时,没有指定std::launch::async策略,那么应该检测wait_for()是否返回std::future_statuc::deferred。例如:
auto f(std::async(task)); //... //如果async()的线程已经启动了,只是还没有运行结束而已,那么才在里面调用while等待async()的结束 if (f.wait_for(std::chrono::seconds(0)) != std::future_status::deferred) { while (f.wait_for(std::chrono::seconds(0)) != future_status::ready) { //... } }
- 引发无限循环的另一个可能原因是:运行此循环的线程完全占用处理器,其他线程无法获得四号时间来备妥future。这回巨幅降低程序速度。最简单的修正就是在循环内调用yield(),或者睡眠一小段时间:
auto f(std::async(task)); //... if (f.wait_for(std::chrono::seconds(0)) != std::future_status::deferred) { while (f.wait_for(std::chrono::seconds(0)) != future_status::ready) { //... std::this_thread::yield(); } }
演示案例
- wait_for()或wait_until()适合书写speculative execution(投机性运行)程序
- 演示案例:
- 下面的bestResultInTime()函数用来返回一个运行结果,quickComputation()用来计算快速运算结果,accurateComputation()用来计算精确结果
- bestResultInTime()函数中等待1分钟async()线程,让其执行accurateComputation(),如果1分钟之后,accurateComputation()执行完那么就返回精确结果,否则就返回快速运算的结果
int quickComputation(); int accurateComputation(); std::future<int> f; int bestResultInTime() { f = std::async(std::launch::async, accurateComputation); //立即异步执行accurateComputation int guess = quickComputation(); //执行quickComputation,获取其返回结果 auto tp = std::chrono::system_clock::now() + std::chrono::minutes(1); std::future_status s = f.wait_until(tp); //等待1分钟 if (s == std::future_status::ready) //如果async()执行结束 return f.get(); //获取accurateComputation的结果并返回 else return guess; //返回quickComputation的结果 }
- 注意future<>不能书写为bestResultInTime()内的局部变量,因为如果声明为局部变量,在bestResultInTime()执行结束之后,future<>的析构函数会阻塞等待accurateComputation()的执行结束,与我们的设计初衷相反了
八、演示案例:开启并等待两个task
代码
- 代码中开启两个后台线程,然后等待两个线程结束并打印一些信息
#include <iostream> #include <thread> #include <future> #include <random> #include <chrono> #include <exception> using namespace std; void doSomething(char c); int main() { std::cout << "starting 2 operations asynchronously" << std::endl; //开启两个后台线程 auto f1 = std::async([] {doSomething('.'); }); auto f2 = std::async([] {doSomething('+'); }); //如果两个线程都已经启动运行了但是还没有结束 if ((f1.wait_for(std::chrono::seconds(0)) != std::future_status::deferred) || (f2.wait_for(std::chrono::seconds(0)) != std::future_status::deferred)) { //那么就调用此while等待两个线程的结束 while ((f1.wait_for(std::chrono::seconds(0)) != std::future_status::ready) && (f2.wait_for(std::chrono::seconds(0)) != std::future_status::ready)) { this_thread::yield(); } } std::cout.put('\n').flush(); //获得两个线程的执行结果 try { f1.get(); f2.get(); } catch (const exception& e) { std::cout << "\nEXCEPTION: " << e.what() << std::endl; } std::cout << "\ndone" << std::endl; } void doSomething(char c) { default_random_engine dre(c); uniform_int_distribution<int> id(10, 1000); //利用随机数,没休眠指定的秒数打印一次字符 for (int i = 0; i < 10; ++i) { this_thread::sleep_for(std::chrono::milliseconds(id(dre))); std::cout.put(c).flush(); } }
演示结果与代码解析
- 在VS下的一次运行结果如下:
- 代码中使用if和whiile等待两个async()线程结束,因此换行符一定是在两个线程都打印完之后才显示,与上图显示的结果相符合
九、async()中callable object的传参
- async()的参数1为callable object(可调用对象),这些可调用对象可以是函数、成员函数、函数对象或lambda,因此我们可以在创建async()时为这些callable object传递参数
调用成员函数
- 如果想要开启一个后台线程去为某个class对象的成员函数,那么需要需要将该对象传递给async()的第二个参数,并且后面的参数为传入的成员函数的参数
- 例如:
class X { public: void mem(int num) {} }; int main() { X x; auto f = std::async(&X::mem,x, 42); //相当于调用x.mem(42); }
传值调用
- 一般的传递方式为传值调用,此时传递的是值的拷贝,而不是引用
- 例如下面是对lambda的传值调用:
void doSomething(char c); //传值调用 auto f1 = std::async([] {doSomething('.'); }); //传值调用 char c = '+'; auto f2 = std::async([=] {doSomething(c); });
- 例如下面是对函数的传值调用:
void doSomething(char c); //传值调用 auto f1 = std::async(doSomething, '.'); //传值调用 char c = '+'; auto f2 = std::async(doSomething, c);
传引用调用
- 我们可以在为可调用对象传参时,为其传入引用
- 例如下面是对lambda的传引用调用:
void doSomething(char c); //传引用调用 auto f1 = std::async([&] {doSomething('.'); }); //传引用调用 char c = '+'; auto f2 = std::async([&] {doSomething(c); });
- 例如下面是对函数的传引用调用:
void doSomething(char c); //传引用调用 char c = '+'; auto f2 = std::async(doSomething, std::ref(c));
传引用的注意事项(并发)
- 如果你使用“传引用调用”的方式去处理一个共享资源,那么需要注意其并发操作
- 演示案例:下面的c字符被async()线程传引用调用,当我们在doSomething()中处理字符c的时候,字符c可能在主线程改变
void doSomething(char c); int main() { char c = '+'; //启动async线程 auto f = std::async([&] {doSomething(c); }); //改变字符c(async()可能还未执行完就被修改了) c = '.'; f.get(); }
- 因此,我们需要使用传引用调用的方式访问共享资源,那么需要做好同步与并发操作,解决方法有:使用mutex或atomic
- 对于callable object的传参建议:
- 没有特殊情况,一般采取传值调用,这样可以避免并发操作
- 如果传参调用导致效率太低,那么可以以const reference的方式传递参数
- 如果真的想要在不同的线程中操作并修改对象,那么需要做好并发操作
十、shared_future
shared_future概述
- future<>的特点:
- std::future一般用来绑定到一个async()上,用来获取异步线程的结果,这通常可以通过future<>.get()来获得
- 但是当一个future绑定到一个async()上时,futture<>只能调用一次get()来获取async()的结果,当第二次调用future<>.get()时会导致不可预期的行为(一般会抛出一个std::future_error异常)
- shared_future<>概述:
- shared_future<>的工作原理与接口与future<>类似,但是机制稍有不同
- 当我们需要多次处理async()异步线程的结果时,例如多个线程都想要获取async()线程的结果。基于这个目的,标准库设计一个std::shared_future<>
- 对于std::shared_future<>,你可以多次调用get()。对于每个get(),你可以获取相同的结果,或抛出异常
shared_future<>.get()的形式
- shared_future<>.get()的接口,其形式①与future<>.get()的形式①稍有不同
- 对于shared_future<>.get()来说,如果async()的callable object有返回值,那么其返回值一定以引用的方式返回(不论是为)
演示案例
#include <iostream> #include <thread> #include <future> #include <random> #include <chrono> #include <exception> using namespace std; int queryNumber(); void doSomething(char c, std::shared_future<int> f); int main() { try { std::shared_future<int> f = std::async(queryNumber); auto f1 = std::async(doSomething, '.', f); auto f2 = std::async(doSomething, '+', f); auto f3 = std::async(doSomething, '*', f); f.get(); f2.get(); f3.get(); } catch (const exception& e) { std::cout << "\nEXCEPTION: " << e.what() << std::endl; } } int queryNumber() { std::cout << "read Number:"; int num; cin >> num; if (!cin) { throw runtime_error("no number read"); } return num; } void doSomething(char c,std::shared_future<int> f) { try { int num = f.get(); for (int i = 0; i < num; ++i) { this_thread::sleep_for(std::chrono::milliseconds(100)); std::cout.put(c).flush(); } } catch (const exception& e) { std::cout << "EXCEPTION in thread " << this_thread::get_id() << ":" << e.what() << std::endl; } }
- 代码解析:
- 我们创建一个async()线程,其运行queryNumber()函数,将其结果保存到一个std::shared_future<>中
- 再创建三个async()线程,让其运行doSomething()函数,其中doSomething()函数内部会调用std::shared_future<>.get()获取其结果
- 运行结果如下:
- 如果你想要获取shared_future<>的引用,可以在定义doSomething()时采取下面的形式:
void doSomething(char c,const std::shared_future<int>& f);