同步和异步
在 C++ 中,同步和异步是描述编程模型和任务执行方式的概念。下表展示了二者的不同特点:
特征 | 同步 (Synchronous) | 异步 (Asynchronous) |
---|---|---|
调用行为 | 函数调用后,调用者需要等待直到操作完成才能继续执行。 | 函数调用后,调用者可以继续执行,不必等待操作完成。 |
执行流程 | 顺序执行。一个任务完成后才能执行下一个任务。 | 可以并行执行多个任务。任务可以独立启动,完成顺序无关。 |
返回结果 | 调用函数后,结果立即返回。调用点获得结果。 | 结果在未来某个时点返回。通常通过回调、事件、std::future 等方式获得。 |
实例 | 直接调用函数:int result = compute(42); | 使用 std::async :auto 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: "
和对应的错误信息。
- 第一个 lambda 表达式对应数据成功获取的情况,它接收一个
类成员函数作为回调函数
直接使用C++
的成员函数作为回调函数将发生错误,因为普通的C++
成员函数都隐含一个传递函数作为参数,即this
指针,C++
通过向其他成员函数传递一个指向自身的指针来实现程序函数访问C++
数据成员。所以实现类成员函数作为回调函数有两种途径:
1、不使用成员函数(使用友元操作符friend
的C
函数访问类的数据成员);
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
内部再调用SPrintText
,SPrintText
转换回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");