--- 摄于 2017 年 9 月 藏川线前段
最近刚完成一个小任务,将 ckb-rust-sdk 由原来的只支持同步操作,改成同时支持异步和同步两种操作,并且保持原有 API 正常运行。任务的需求非常清晰,只是实现起来会碰到一些问题,实现路径也是多样的,虽然最后的效果一致,但实现的代价是不同的,并且在实现过程中,还碰到了不知道是 tokio 还是 rust std 在 windows 下的兼容问题。具体实现可以看这里:
按大类来分,可以分为以下几种:
同步和异步分别完全实现一遍,耗资巨大
底层用同一个同步实现
底层用同一个异步实现
同步版本用 block_on + current runtime 原地等待完成(需要限制 Future: Send)
同步版本用 channel + global runtime 的方式等待任务完成(需要限制 Future: Send + &’static)
同步版本用 thread_park as waker + timeout + loop 的方式模拟完成
我们默认讨论的 runtime 是 tokio 而不是 async-std,因为它俩在处理任务调度的时候有一些很明显的区别,尤其是当任务发生 blocking 的情况下,两者的处理是完全不一样的:
block_in_place
的方式,让确定性 blocking 的场景正常执行,并将其执行 worker 线程逐出 worker 队列最后选择了 block_on + current runtime 的实现方式,仅仅是因为 dyn trait + lifetime 的限制导致不得不这么做。
历史代码中有核心 trait 需要转换成 dyn trait 进行操作,这个实现依赖导致无法让核心 trait 返回 impl Future,因为 dyn trait 需要保证 object safe。
核心 trait 无法返回 impl Future 的前提下,让 trait 支持 async 方法需要 async_trait 库支持,因为目前 rust trait 支持的 async 方法暂时无法满足需求,而 async_trait 实现的返回值中,Future 与 trait 的实现方有 lifetime 的绑定,导致无法实现 static 的 Future,这样这些 Future 就无法通过 channel 进行传递,最多能实现到 Future: Send 的级别
sdk 代码中依赖了几个 trait 是上游库的实现,只有同步接口,这样必须在使用它的时候,实现异步方法原地完成的操作
那么最后实现该 Future 转成同步实现的核心函数就是这样:
fn block_on<F: Send>(future: impl Future<Output = F> + Send) -> F {
match tokio::runtime::Handle::try_current() {
Ok(h)
if matches!(
h.runtime_flavor(),
tokio::runtime::RuntimeFlavor::MultiThread
) =>
{
tokio::task::block_in_place(|| h.block_on(future))
}
// if we on the current runtime, it must use another thread to poll this future,
// can't block on current runtime, it will block current reactor to stop forever
// in tokio runtime, this time will panic
Ok(_) => std::thread::scope(|s| {
s.spawn(|| {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(future)
})
.join()
.unwrap()
}),
Err(_) => tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(future),
}
}
先判断是多线程 runtime 还是单线程 runtime,多线程原地 block_in_place
操作,单线程直接用 std::thread::scope
将 Future 放到新线程里用单线程 runtime 执行完成,如果不在 runtime 内,就直接用单线程 runtime 执行完成。
上面这套东西本来没问题,但是,当使用 thread_local
进行初始化单线程 runtime 的时候,在 windows 环境下,会碰到 thread local drop runtime 的时候卡死问题,当且仅当 runtime 中使用了 spawn_blocking
的时候才出现。这个问题导致不得不舍弃 thread local 的写法。
最后的实现效果是,使用异步接口进行操作,用户得到的是一个完整的大 Future;使用同步接口操作的话,每一个同步接口都会是 block_on 一个 Future 执行。
今天刚好是除夕,祝大家新年快乐、身体健康、万事如意。
请登录后评论
评论区
加载更多