时机永远很重要。
先来一段代码
seastar::future<> fail() {
throw std::exception();
}
seastar::future<> f() {
return fail().finally([] {
std::cout << "cleaning up\n";
});
}
试问
finally()
里面的代码会执行吗?答案是:“不会”。为什么呢?因为按照 C++
的话说,fail().finally(…)
是个表达式。表达式的结构类似:
表达式的参数在求值之前都会准备好。但是求值的过程有点像深度优先的遍历。但是又不完全是,遍历的顺序同时需要遵循
C++ 对各类表达式中的子表达式的求值顺序的要求。比如说,在
C++17
之后,函数调用表达式中,其左边的参数即函数本身,需要在参数之前求值完毕,诸参数的求值顺序则没有要求。当所有参数都备好之后,再对函数调用,即
()
这个操作进行求值。如果在求值过程中抛出异常,那么我们就会走常规流程,比如说所有的变量都会销毁。而最顶层的函数调用因为其参数还没有准备好,所以也无法开始其求值的过程。整个表达式在一个
exception
前瞬间土崩瓦解。因此上面的
f()
无法 返回
seastar::future<>
,而是给调用方抛出
exception
。
再看看下面的代码:
seastar::future<> fail() {
throw std::exception();
}
seastar::future<> f() {
return seastar::sleep(1s).then([] {
return fail();
}).finally([] {
std::cout << "cleaning up\n";
});
}
这段代码中的 finally()
就会被调用。因为
then()
后面的 lambda
表达式在求值的时抛出的异常会被我们的 continuation
实现捕捉住,放到对应的 promise 中,确保它返回的 future 的
finally()
函数调用能对其做出处理。
其实这个 caveat 在 Seastar 的文档 里面有专门的章节解释。但是我之前并没有在意。
在此引以为戒。