上一篇文章中(使用QFutureInterface编写异步操作)提到了future-promise的并行计算开发模型,但其中的Promise部分并不完整。实际上,除了设置future的值之外,很多现代开发语言的Promise还有串接异步操作的功能。本文将给大家介绍一种非常简单实用的串接方法。
何为异步操作串接?
大家在编写稍微复杂点的异步操作时就会遇到这样的需求:我有多个异步操作,比如说有A、B、C三个,我需要A完成后以它的结果作为输入立即执行B,B执行完后再以它的结果作为输入立即执行C,最后C的结果才是我想要的结果,如下图:
这就是异步操作串接,或者叫异步链(Async chain)。
此外,随着加入错误处理分支,异步链会变得更复杂。
目前在js中,处理异步链最常见的方法是回调函数。例如下面是一个典型的通过回调函数来处理异步链的代码:
file.read("xyz.txt",
function(content){
console.log("read OK!");
},
function(error){
console.log("error:", error);
}
);
这段代码通过read
读取一个文件,其中:
- 第一个参数是文件的路径。
- 第二个参数是一个回调函数,当读取成功的时候会被调用,同时文件内容将会以参数的形式传给它。
- 第三个参数也是个回调函数,当读取失败的时候会被调用,同时错误信息将会以参数的形式传给它。
如果回调函数也是异步操作呢?我们可以以嵌套的方式加入第二层回调函数,以此类推。所以我们上面的例子用回调函数的方式写出来大概是下面这样子:
A(paramsForA,
function(resultFromA){
B(resultFromA,
function(resultFromB){
C(resultFromB,
function(resultFromC){
// 最后拿到我们想要的结果
},
function(errorFromC){
// 对C操作的错误进行处理
}
);
},
function(errorFromB){
// 对B操作的错误进行处理
}
);
},
function(errorFromA){
// 对A操作进行错误处理
}
);
是不是开始有些乱了?这个异步链才三步,实际开发中十几步的异步链也是很常见的,比如下面这个异步链:
可以想到这个嵌套回调得有多复杂,最终我们会进入一个暗黑名词:回调地狱(Callback Hell)。网上找了个夸张的图,大家体会下:
回调地狱在js的开发圈已经是个公认的问题,有人甚至专门建了个网站来讲述这个问题(当然明显讲得比我好):Callback Hell
回调地狱的大杀器:Future/Promise
学过Haskell或者lisp这种语言的同学可能会发现,这种回调地狱怎么这么眼熟?
是的,那种五六个、十几个花括号嵌套的情况,在函数式的编程语言里太常见。而js回调函数的写法,其实就是一种函数式编程:函数作为参数传给另外一个函数。
而future/promise是一种面向对象的、基于事件驱动的编程模型。需要注意的是,好多语言、框架都有future/promise的实现,本文更多的是基于该编程模型的一种简单实践,和读者用过的其他实现可能有出入。咱们领悟大意就好。
首先介绍下这两个概念:
future:表示异步操作的结果。异步操作执行后会立即返回一个future,但future的值只有等该异步操作执行完后才有。
promise:表示异步操作的一种承诺/互联,它有三种状态:
我们约定:promise构建时处于undertermined(或者叫undefined),然后可以转入resolved或者rejected状态,只能转入其中一个并且不能再改变状态。
它用来做以下几件事情:
- 设置当什么时候变成resolved状态;
- 设置当什么时候变成rejected状态;
- 当变成resolved状态后干什么;
- 当变成rejected状态后干什么。
使用future/promise编程模型一般分成两步:
- 串接,即生成各个需要的promise,以及设置好每个promise的上面四件事情;
- 点火
具体实现
在Qt中我们有现成的future类:QFuture
。我们直接使用它。promise需要我们自己实现。不过Qt有一个QFutureWatcher
,可以在QFuture
的基础上提供信号与槽的接口。我们基于这个类实现我们的promise。
class SimplePromise : public QObject
{
Q_OBJECT
public:
typedef QFutureWatcher<Result> AsyncResultWatcher;
enum Status{
Undefined,
Pending,
Resolved,
Rejected
};
Q_ENUM(Status)
Q_PROPERTY(Status status READ status WRITE setStatus NOTIFY statusChanged)
explicit SimplePromise(QObject *parent = nullptr);
Status status() const;
void setStatus(Status status);
signals:
void resolved(QVariantList results);
void rejected(QString reason);
void statusChanged(Status status);
public slots:
void resolveWhen(QVariant prerequsite);
void resolveWhen(QVariantList prerequsites);
void resolve(QVariantList results);
void reject(QString reason);
void onResolved(QJSValue callable);
void onRejected(QJSValue callable);
private:
void prerequsiteResolved(int idx, QVariant result);
private:
Status m_status = Undefined;
int m_totalCount = 0, m_currentCount = 0;
QVector<QVariant> m_results;
QJSValue m_resolvedHandler, m_rejectedHandler;
};
SimplePromise::Status
是promise的状态枚举。为了检测异常,我多加了一个Pending
状态,用于表示Undefined
到resolved
或者rejected
转变中的状态。
onResolved
和onRejected
分别用于设置当promise变成resolved或者rejected时执行的函数。因为面向Qt Quick,所以这边的参数都是QJSValue
类型。
看下实现,主要是resolveWhen
函数:
void SimplePromise::resolveWhen(QVariantList prerequsites)
{
if(m_status != Undefined){
throw PromiseException("status invalid!");
}
m_currentCount = 0;
m_totalCount = prerequsites.size();
m_results.resize(m_totalCount);
for(auto i = 0; i != m_totalCount; ++i){
const auto variant = prerequsites[i];
auto const t = int(variant.type());
if(t == QMetaType::QObjectStar){
auto promiseObj = qobject_cast<SimplePromise*>(variant.value<QObject*>());
if(promiseObj != nullptr){
connect(promiseObj, &SimplePromise::resolved, this, [i, this](QVariantList results){
if(status() == Pending){
auto var = QVariant::fromValue(results);
prerequsiteResolved(i, var);
}
});
connect(promiseObj, &SimplePromise::rejected, this, [&](QString reason){
reject(reason);
});
}else{
throw PromiseException("resolveWhen can only handle future or promise type!");
}
}
else if(t == RESULT_TYPE){
auto watcher = new AsyncResultWatcher(this);
connect(watcher, &AsyncResultWatcher::finished, [watcher, i, this]{
auto result = watcher->result();
if(result.success()){
prerequsiteResolved(i, result.data());
}
else{
reject(result.reason());
}
});
watcher->setFuture(variant.value<QFuture<Result> >());
}
}
setStatus(Pending);
}
我们的promise类支持链接QFuture
和SimplePromise
,这样我们可以在串接的时候灵活选择串接上一步的future还是promise。原则是:
- 如果是在一步异步出发的时候运行多个异步操作,则需要使用所有这些异步操作的future作为
resolvedWhen
的参数; - 如果是多步异步操作出发的异步操作(也就是某个后续异步操作需要等待多个并发异步操作返回之后再运行),则需要使用这些异步操作的promise作为
resolveWhen
的参数。
实例运行
回到我们之前的异步链例子,可以确定A和D是点火位置:
我们编写A~F六个测试异步操作,内部无非打印一些信息,然后sleep一会儿当前线程,比如A和E操作:
QFuture<Result> FluxHub::asyncFuncA(QString msg)
{
auto future = QtConcurrent::run([](QString msg)->Result{
qDebug()<<"async func A running...";
QThread::currentThread()->msleep(3000);
Result result(QVariant::fromValue(msg + " funcA"));
qDebug()<<"async func A stopped...";
return result;
}, msg);
return future;
}
QFuture<Result> FluxHub::asyncFuncE(QString msg, int val)
{
auto future = QtConcurrent::run([](QString msg, int val)->Result{
qDebug()<<"async func E running...";
QThread::currentThread()->msleep(3000);
Result result(QVariant::fromValue(msg + " funcE" + QString(" %1").arg(val)));
qDebug()<<"async func E stopped...";
return result;
}, msg, val);
return future;
}
javascript代码如下:
var ap= $hub.promise(), bp = $hub.promise(), cp = $hub.promise();
var dp = $hub.promise(), ep = $hub.promise(), fp = $hub.promise();
ap.onResolved(function(param){
var bf = $hub.asyncFuncB(param); bp.resolveWhen(bf);
});
bp.onResolved(function(param){
var cf = $hub.asyncFuncC(param); cp.resolveWhen(cf);
});
ep.resolveWhen([cp, dp]);
ep.onResolved(function(param1, param2){
var ef = $hub.asyncFuncE(param1, param2); fp.resolveWhen(ef);
});
fp.onResolved(function(param){
console.log("final result:", param);
// process final result here...
});
// fire on!
ap.resolveWhen($hub.asyncFuncA("X"));
dp.resolveWhen($hub.asyncFuncD(8));
最后运行效果如下面的视频:
从视频上可以看到,最后运行的顺序就是我们图中描述的顺序。使用future/promise编程模型可以让我们的异步代码看起来清晰明了,避免了回调地狱。
我把SimplePromise
的简单实现以及测试工程放到了GitHub上:
大家可以clone下来然后运行我上面的js代码,体会下future/promise编程。
Promise/A+标准
最后需要特别指出的是,Promise/A+是有一个标准的,网址在:Promise/A+标准。本文只是实现了其中的一小部分接口。有兴趣的同学可以在此基础上尝试适配所有标准。