C++ 回调与std::async

同步和异步

在 C++ 中,同步和异步是描述编程模型和任务执行方式的概念。下表展示了二者的不同特点:

特征同步 (Synchronous)异步 (Asynchronous)
调用行为函数调用后,调用者需要等待直到操作完成才能继续执行。函数调用后,调用者可以继续执行,不必等待操作完成。
执行流程顺序执行。一个任务完成后才能执行下一个任务。可以并行执行多个任务。任务可以独立启动,完成顺序无关。
返回结果调用函数后,结果立即返回。调用点获得结果。结果在未来某个时点返回。通常通过回调、事件、std::future 等方式获得。
实例直接调用函数:int result = compute(42);使用 std::asyncauto fut = std::async(compute, 42);
资源消耗通常消耗较少资源。可能因线程创建和上下文切换而消耗更多资源。
等待操作调用阻塞直到操作完成。调用一般不阻塞,可能需要某种形式的等待机制来获取结果。
错误处理错误直接由调用栈传递,通常通过异常处理。错误需要通过特殊机制进行传递和处理,可能包括在 std::future 中捕获异常。
编程复杂性相对简单,因为流程是线性的。可能比较复杂,特别是当涉及到多个并发任务交互时。
适用场景耗时较短的操作,或需要立即结果的场景。耗时较长的操作,I/O操作,网络请求或其他需要避免UI阻塞的场景。
控制流线性控制流。需要额外的控制逻辑来管理并发和任务完成时的行为。
示例代码int compute(int x) { /* ... */ } int result = compute(42);std::future<int> computeAsync(int x) { /* ... */ }
阻塞和非阻塞阻塞调用。非阻塞调用,但获取结果时可能阻塞(如通过 std::future::get())。
主要支持的特性函数调用、循环、条件语句。并发算法、线程、任务管理器、非阻塞I/O。
并发和并行的支持能力无或有限。可以实现真正的并发和并行执行。

异步

特性回调 (Callback)std::async
概念函数指针或可调用对象,由用户提供,某个操作完成时被调用。一个函数,用于启动一个异步任务,返回 std::future 对象用于访问结果。
异步性明确,通过在事件完成时调用提供的函数实现。明确,调用 std::async 会创建一个异步任务。
使用简易性较低,需要管理回调函数和可能的状态维护。较高,std::async 通过返回的 std::future 管理状态和结果。
并发控制由用户控制,需要小心处理资源共享和同步。由 std::future 和其它并发机制(比如 std::promise)管理。
结果获取结果通过回调函数的参数传递,或需通过共享状态管理。结果通过 std::future::get() 获取,若结果未准备好则阻塞调用者。
错误处理异常的传递和处理可能更复杂,需要额外的设计。异常被封装在 std::future 中,可以在调用 get() 时抛出,简化了错误处理。
适用场景事件驱动的编程模型,如 GUI 事件处理、网络请求回调。CPU 密集型操作、需要结果返回的异步执行任务。
线程管理需自行管理或配合事件循环使用,并非总是涉及多线程。自动管理线程,std::async 可以选择立即或延迟在新线程中执行任务。
取消操作取消需用户自行实现,可能通过共享状态来检测取消信号。C++ 标准不直接支持取消 std::async 任务,但可通过 std::future 状态管理实现。

回调函数

回调函数定义

声明并定义一个函数A,然后把函数A的指针作为参数传入其他的函数(或系统)中,其他的函数(或系统)在运行时通过函数指针调用函数A,这就是所谓的回调函数。

回调函数示例
lambda表达式作为回调函数
void fetchData(std::function<void(std::string)> on_success, std::function<void(std::string)> on_error) {
    // 模拟异步数据获取
    if(/* data fetch successful */) {
        on_success("Data");
    } else {
        on_error("Error");
    }
}

int main() {
    fetchData(
        [](std::string data) { std::cout << "Success: " << data << std::endl; }, // 成功回调
        [](std::string error) { std::cout << "Error: " << error << std::endl; }  // 错误回调
    );
}

fetchData 函数

  • fetchData 函数接受两个参数,类型均为 std::function<void(std::string)>。这意味着这两个参数都是可以接受一个 std::string 类型参数的函数,且没有返回值。这两个参数分别对应于数据获取成功和失败的回调处理函数。
  • 函数体内部包含了一个条件判断,用于模拟数据获取的成功或失败。这里使用了伪代码 /* data fetch successful */ 作为条件判断的占位符,表示这里需要根据真实的数据获取逻辑来判断成功还是失败。
    • 如果数据获取成功(即条件判断为真),则调用 on_success 回调,传递成功获取的数据(在这个例子中,传递的是字面量 "Data")。
    • 否则,调用 on_error 回调,传递错误信息(在这个例子中,传递的是字面量 "Error")。

main 函数

  • 在 main 函数中,调用 fetchData 函数,并传递两个 lambda 表达式作为参数。这两个 lambda 表达式分别对应数据获取的成功和失败情况的处理逻辑。
    • 第一个 lambda 表达式对应数据成功获取的情况,它接收一个 std::string 类型的参数(在这里是成功获取的数据),并打印出 "Success: " 和对应的数据。
    • 第二个 lambda 表达式对应数据获取失败的情况,它同样接收一个 std::string 类型的参数(在这里是错误信息),并打印出 "Error: " 和对应的错误信息。
类成员函数作为回调函数

直接使用C++的成员函数作为回调函数将发生错误,因为普通的C++成员函数都隐含一个传递函数作为参数,即this指针,C++通过向其他成员函数传递一个指向自身的指针来实现程序函数访问C++数据成员。所以实现类成员函数作为回调函数有两种途径:

1、不使用成员函数(使用友元操作符friendC函数访问类的数据成员);

2、使用静态成员函数。

#include <iostream>
​
class CPrintString
{
public:
    void PrintText(const char *str)
    {
        std::cout << str << std::endl;
    }
// 传入的 pPs 指针的作用是允许从静态函数的上下文访问类的非静态成员。
// 由于静态成员函数不属于类的某个特定对象,而是属于类本身,因此不含有隐式的 this 指针。要在静态函数中调用非静态成员,我们需要一个对类实例的引用或指针。
    static void SPrintText(void *pPs, const char *str)
    {
        CPrintString *pThis = static_cast<CPrintString *>(pPs);
        if(NULL == pPs)
        {
            return;
        }
        pThis->PrintText(str);
    }
};
​
typedef void (*PRINTTEXT)(void *pPs, const char *str);
// 不使用 typedef 的情况下,如果你要声明一个函数指针,你需要这样做:
// void (*myFunctionPointer)(void *pPs, const char *str);
// 使用 typedef 的情况下:
// PRINTTEXT myFunctionPointer;

​
void CallBackFun(void *pPs, const char *str, PRINTTEXT fp)
{
    fp(pPs, str);
}
​
int main()
{
    CPrintString obj;
    CallBackFun((void *)&obj, "Hello world.", CPrintString::SPrintText);

    return 0;
}

这段代码展示了如何在 C++ 中实现和使用回调函数的一个实例。代码通过定义一个简单的类 CPrintString 和它的方法来打印传入的字符串,进一步演示了如何使用静态成员函数作为回调。下面是对代码的分解和解释:

类定义及成员函数

  • CPrintString 类拥有一个公共成员函数 PrintText 用于输出字符串。
  • 类还具有一个静态成员函数 SPrintText,作为一个包装器(wrapper)允许通过调用PrintText 来实现接口兼容,使得可以使用它作为回调函数。静态成员函数可以被直接调用而无需类的实例化对象,但它需要显式地将 void* 类型的参数转换回 CPrintString* 类型,才能调用非静态成员函数。

类型定义

  • PRINTTEXT 是一个函数指针类型,接受一个 void* 参数和一个 const char* 参数,并返回 void。这为回调函数提供了一种类型定义,方便之后的使用。

回调函数的实现

  • CallBackFun 是一个接受回调函数的函数。它接收三个参数:一个 void*类型,用于传递对象指针;一个字符串;一个回调函数指针。在这个函数内部,通过该函数指针调用实际的回调函数。

main 函数中的使用

  • 在 main 函数中,创建了 CPrintString 类的一个实例 obj
  • 然后调用 CallBackFun 函数,并将对象 obj 的地址、一个字符串 "Hello world." 和指向 CPrintString::SPrintText 的函数指针传递给它。
  • CallBackFun 内部再调用 SPrintTextSPrintText 转换回 CPrintString* 类型并调用 PrintText 方法,最终输出字符串 "Hello world."
回调函数的作用

在回调中,主程序把回调函数像参数一样传入库函数。这样一来,只要我们改变传进库函数的参数,就可以实现不同的功能,并且丝毫不需要修改库函数的实现,这就是解耦。再仔细看看,主函数和回调函数是在同一层的,而库函数在另外一层,一般情况下库函数对开发人员并不可见,库函数的实现一般不会被修改,也就是说不能通过修改库函数让库函数调用普通函数那样实现,那就只能通过传入不同的回调函数了,这在企业开发中非常常见。

async

定义

std::async 是一个模板函数,接收一个回调(回调函数或可调用对象)作为参数,并异步执行。

template <class Fn, class... Args>
future<typename result_of<Fn(Args...)>::type> async (launch policy, Fn&& fn, Args&&... args);

std::async 会返回一个 std::future<T> ,其存储 std::async() 调用的函数对象的返回值。回调函数的参数在函数指针参数的后面传入。

std::async 的第一个参数是 launch policy ,其控制 syd::async 的异步行为。有三种 launch policy 可选:

  • std::launch::async :保证行为是异步的 - 传入函数在单独的线程中执行
  • std::launch::deferred :行为是非异步的 - 会在其他线程调用 future 的 get() 时被调用传入的回调函数
  • std::launch::async | std::launch::deferred :程序会根据系统情况自动决定是同步还是异步,开发者无法手动控制。

如果不指定 launch policy ,默认行为与 std::launch::async | std::launch::deferred 类似。本文接下来只讲解 std::launch::async 。

回调函数可以是函数指针、函数对象和 lambda 表达式。

使用场景

假设我们要从数据库获取一个字符串,再从文件系统中获取一个字符串,再把这两个字符串合并到一起。如果使用单线程做这件事,可以用如下代码:

#include <iostream>
#include <string>
#include <chrono>
#include <thread>
​
using namespace std::chrono;
​
std::string fetchDataFromDB(std::string recvdData)
{
    // 模拟耗时的数据库查询操作,让该函数运行五秒
    std::this_thread::sleep_for(seconds(5));
​
    return "DB_" + recvdData;
}
​
std::string fetchDataFromFile(std::string recvdData)
{
    // 模拟耗时的本地数据读取操作,让该函数运行五秒
    std::this_thread::sleep_for(seconds(5));
​
    return "File_" + recvdData;
}
​
int main()
{
    // 获取开始时间
    system_clock::time_point start = system_clock::now();
​
    // 从数据库取回数据
    std::string dbData = fetchDataFromDB("Data");
​
    // 从本地文件取回数据
    std::string fileData = fetchDataFromFile("Data");
​
    // 获取结束时间
    auto end = system_clock::now();
​
    auto diff = duration_cast < std::chrono::seconds > (end - start).count();
    std::cout << "总耗时 = " << diff << " 秒" << std::endl;
​
    // 合并数据
    std::string data = dbData + " :: " + fileData;
​
    // 打印合并后的数据
    std::cout << "Data = " << data << std::endl;
​
    return 0;
}

输出:

总耗时 = 10 秒
Data = DB_Data :: File_Data

两个函数各耗时五秒,串行执行的结果就是耗时 10 秒。

由于读数据库和读本地文件系统是相互独立的,所以应该可以通过并行化来节约时间。

一种方法是创建一个新线程,为该线程函数传入一个 promise 参数,并通过与之关联的 std::future 获取数据。

但还有个更简单的方法,就是使用 std::async

用函数指针回调函数来调用 std::async

将上面的代码修改为使用 std::async() 异步调用 fetchDataFromDB() 函数

std::future<std::string> resultFromDB = std::async(std::launch::async, fetchDataFromDB, "Data");
// 做些其他操作 
// 从数据库获取数据
// 代码会在此处阻塞,直到 future<std::string> 对象中的数据就绪
std::string dbData = resultFromDB.get();

std::async 会做下面三件事:

  • 自动创建一个新线程(或者是从其内部的线程池中拿一个线程)和一个 promise 对象
  • 向新线程中的函数传入 std::promise 对象,并返回与之关联的 std::future 对象
  • 在函数运行结束后,设置 std::promise 对象的值,我们即可通过 std::future 获取返回值

完整的代码示例如下:

#include <iostream>
#include <string>
#include <chrono>
#include <thread>
#include <future>
​
using namespace std::chrono;
​
std::string fetchDataFromDB(std::string recvdData)
{
    // 模拟耗时的数据库查询操作,让该函数运行五秒
    std::this_thread::sleep_for(seconds(5));
​
    return "DB_" + recvdData;
}
​
std::string fetchDataFromFile(std::string recvdData)
{
    // 模拟耗时的本地数据读取操作,让该函数运行五秒
    std::this_thread::sleep_for(seconds(5));
​
    return "File_" + recvdData;
}
​
int main()
{
    // 获取开始时间
    system_clock::time_point start = system_clock::now();
​
    std::future<std::string> resultFromDB = std::async(std::launch::async, fetchDataFromDB, "Data");
​
    // 从本地文件获取数据
    std::string fileData = fetchDataFromFile("Data");
​
    // 从数据库获取数据
    // 代码会在此处阻塞,直到 future<std::string> 对象中的数据就绪
    std::string dbData = resultFromDB.get();
​
    // 获取结束时间
    auto end = system_clock::now();
​
    auto diff = duration_cast < std::chrono::seconds > (end - start).count();
    std::cout << "总耗时 = " << diff << " 秒" << std::endl;
​
    // 混合数据
    std::string data = dbData + " :: " + fileData;
​
    // 打印混合数据
    std::cout << "Data = " << data << std::endl;
​
    return 0;
}

这段代码只会耗时五秒。输出如下:

总耗时 = 5 秒
Data = DB_Data :: File_Data

使用函数对象作为回调函数,调用 std::async
/*
 * 函数对象
 */
struct DataFetcher
{
    std::string operator()(std::string recvdData)
    {
        // 模拟耗时操作,需要运行五秒
        std::this_thread::sleep_for (seconds(5));
        // 做一些获取数据相关的操作
        return "File_" + recvdData;
​
    }
};
​
// 使用函数对象调用 std::async
std::future<std::string> fileResult = std::async(DataFetcher(), "Data");

使用 Lambda 表达式作为回调函数,调用 std::async
// 使用 Lambda 表达式作为回调函数,调用 std::async
std::future<std::string> resultFromDB = std::async([](std::string recvdData){
                        std::this_thread::sleep_for (seconds(5));
                        // 做一些数据库查询相关的操作
                        return "DB_" + recvdData;
                    }, "Data");
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

**K

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

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

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

打赏作者

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

抵扣说明:

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

余额充值