C++(标准库):44---并发之(高级接口async()、futrue、shared_future)

一、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);

 

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

董哥的黑板报

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值