藏川线前段

--- 摄于 2017 年 9 月 藏川线前段

最近刚完成一个小任务,将 ckb-rust-sdk 由原来的只支持同步操作,改成同时支持异步和同步两种操作,并且保持原有 API 正常运行。任务的需求非常清晰,只是实现起来会碰到一些问题,实现路径也是多样的,虽然最后的效果一致,但实现的代价是不同的,并且在实现过程中,还碰到了不知道是 tokio 还是 rust std 在 windows 下的兼容问题。具体实现可以看这里:

实现方案

按大类来分,可以分为以下几种:

我们默认讨论的 runtime 是 tokio 而不是 async-std,因为它俩在处理任务调度的时候有一些很明显的区别,尤其是当任务发生 blocking 的情况下,两者的处理是完全不一样的:

核心实现

最后选择了 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 执行。

最后

今天刚好是除夕,祝大家新年快乐、身体健康、万事如意。

评论区

加载更多

登录后评论