异步核心

executor 还是 reactor?

  1. executor: 在 Rust 中,调度 Future 对象的是一个 Executor 线程,它会不停地轮询 Future 的状态,一旦 Future 对象的状态由 “就绪” 变为 “完成”,Executor 就会把结果返回给调用方。异步计算需要包含可中止、存储当前状态和恢复状态这些特征,这些特征都是由 Future trait 统一实现的,这样在使用时避免了许多重复性的工作。Future trait 的 poll 方法会返回一个 Poll 枚举,它包含两个成员:Ready 和 Pending。
  2. Reactor: 在 Reactor 模型中,主线程通过事件循环来监听异步事件,并在事件发生时调用相应的回调函数。和任务执行不同,整个异步过程是由事件驱动的,主线程会等待事件发生,并且不断轮询异步事件队列。当异步操作完成并返回结果时,主线程会在回调函数中处理结果。Reactror 模型的一个关键点是将事件相关联的回调函数保存在异步队列中,当事件被触发时,会根据事件类型将对应的回调函数压入事件循环队列,然后一次性地调用所有的回调函数。这样做的好处是可以减少线程切换的开销,但也会引入一些问题,比如回调函数的执行时间过长, 会导致其他回调函数的执行被阻塞。为了解决这个问题,可以将回调函数的执行放到线程池中,这样就可以避免阻塞主线程。

rust的future/async/await如何运行

执行 main 函数程序结束执行 main 函数程序结束执行异步函数1执行异步函数2程序执行到等待语句,挂起并将控制权交给 Io 线程异步函数1完成计算并返回结果异步函数2完成计算并返回结果执行异步函数1执行异步函数2将控制权交给 Io 线程异步函数1完成计算并返回结果异步函数2完成计算并返回结果将挂起点打包成 Generator 并存储在 Future 对象中轮询 Future 1,处理中断点的返回值轮询 Future 2,处理中断点的返回值轮询 Future,检查状态是否已转变轮询 Future 1,处理中断点的返回值轮询 Future 2,处理中断点的返回值处理异步函数1的返回结果处理其他 Future 的计算结果处理其他 Future 的计算结果将计算结果返回给 Executor 线程(Future 2 计算完成)继续执行 Future 1 的计算继续执行 Future 2 的计算下一轮切换到 Executor 线程事件1发生,将回调函数1放入事件循环队列事件2发生,将回调函数2放入事件循环队列调用下一个回调函数注册异步事件1,将回调函数1加入异步队列注册异步事件2,将回调函数2加入异步队列开始事件循环执行回调函数1轮询 Future的状态将结果返回给回调函数1并执行之后的操作执行回调函数2轮询 Future的状态将结果返回给回调函数2并执行之后的操作处理异步函数1的返回结果处理其他 Future 的计算结果处理其他 Future 的计算结果Main ThreadAsync ThreadExecutor ThreadIo ThreadReactor Thread

Executor 模式和 Reactor 模式本质上都是为了实现异步编程而设计的模式,它们使用不同的机制来处理异步调用。尽管都涉及多线程,但它们的实现方式有很大的不同,不应简单地认为是多线程的组合。

Executor 模式是采用线程池的方式管理多个子线程来执行异步任务,并在主线程上等待所有的任务完成。在 Rust 的 Executor 模式中,主线程通过轮询 Future 对象的方式等待异步任务完成,并不涉及多线程的处理,I/O 操作使用了操作系统提供的异步调用方式,通过信号通知主线程任务完成。在其他编程语言中,Executor 模式可能需要使用多个线程来处理任务执行。

而 Reactor 模式采用了单线程的方式,在主线程上监听异步事件以及相应的回调函数,并在事件触发后调用回调函数来处理异步任务。Reactror 模式的优势在于不会引入线程切换的开销,当然也存在处理IO密集型任务的性能瓶颈,但异步执行的优势在于任务不需要等待,可以使用事件驱动时间进行并行处理。

所以,Executor 模式和 Reactor 模式是两种不同的异步编程模型,它们在实现方面有明显的不同。虽然它们都涉及多线程,但其中 Executor 模式使用线程池管理多个异步任务,而 Reactor 模式使用事件循环监听异步事件并调用相应的回调函数,具体实现上不尽相同。

你只管调用,与系统交互交给异步运行时

在 Rust 中,异步运行时是一个独立的运行时环境,它负责管理异步任务的执行,包括任务的调度、线程池的管理、任务的执行等。

Rust 的异步运行时并没有直接暴露出创建底层线程的接口,而是提供了一些抽象接口和默认实现,以方便用户创建和管理异步任务。如果需要深入了解 Rust 异步运行时的具体实现方式,可以参考 tokio 等库的源码。

以tokio异步运行时为例

创建 Runtime 对象获取 handle向待执行队列中提交任务执行异步操作并处理任务结果关闭 Runtime 对象并释放资源创建线程池等待待执行任务队列中的任务将任务加入待执行队列从待执行队列中取出任务处理异步委托任务的返回结果检查待执行队列是否为空,并处理挂起事件等待文件IO事件执行 IO 事件处理器等待运行时发送的异步事件执行异步事件异步任务执行完成Main ThreadRuntime ThreadIO ThreadAsync Thread

在 tokio 的运行时中,线程的创建和销毁、任务的调度和执行都是在 runtime 自己的线程池中完成的。具体流程如下:

  1. 创建 runtime:在 Builder::new() 方法中调用一系列方法,比如 num_threads()、threaded_scheduler(),以设置线程池大小和线程调度器类型等属性。最终通过 build() 方法创建 runtime 对象。
  2. 获取 handle:通过 handle() 方法获取 runtime handle,用于在运行时中 spawn 新的 future 和执行异步操作。
  3. Spawn future:通过 spawn() 方法异步地启动一个 future,并返回一个 JoinHandle,用于跟踪 future 的执行状态。此时 future 并不会被直接执行,而是进入到待执行队列中。
  4. Run tasks:在 block_on() 方法中,runtime 会持续地从待执行队列中取出 future 并执行,直到所有任务都完成为止。
  5. Shutdown runtime:通过 drop(rt) 方法关闭 runtime,释放所有资源,包括线程池、任务队列、调度器等。

异步运行时与系统交互

下面是一个概括性的异步运行时如何与系统底层交互的流程:

  1. 创建运行时对象:在运行时初始化过程中,需要创建并初始化一些系统资源,比如文件描述符、线程池、事件循环等。在这个过程中,需要调用系统底层接口来申请和释放这些资源,并设置相应的事件处理器(比如 epoll 或 kqueue)。

  2. 注册/注销事件:在执行异步任务的过程中,需要对文件 IO 或网络 IO 等事件进行处理。为了实现高效地异步 IO,可以使用系统底层提供的事件处理器来监听文件描述符和网络端口等事件,并在事件发生时及时通知异步运行时。这个过程需要调用系统底层标准库方法,比如 epoll_ctl 等。

  3. 调用异步模块:当程序使用异步模块来发起异步操作请求时,通常是将请求的状态信息(比如回调函数、数据缓冲区等)传递给异步运行时。异步运行时会将这些状态信息保存到一个任务队列中,以便在事件就绪后及时调用相应的回调函数进行异步操作。

  4. 处理异步事件:当异步请求的 IO 事件发生时,事件处理器会向异步运行时发送通知,并触发异步事件处理函数的执行。在这个过程中,异步运行时需要调用系统底层提供的标准库方法,比如 poll 等,来获取异步 IO 事件的状态和数据,并将其传递给相应的回调函数。如果异步请求执行完毕,异步运行时也需要从任务队列中移除对应的状态信息,以释放底层资源并回收内存。

一个更加简化的异步运行时泳道图

创建运行时对象调用异步模块处理异步事件移除对应状态信息注册事件触发异步事件运行时对象异步模块事件处理器

对比python和golang

future对比

语言
Future/Async/Await执行方式
优点
缺点
Python
通过事件循环实现Future/Async/Await的执行
异步编程模型简单易学,基于asyncio库提供了一整套异步 I/O 操作的API,更容易上手和维护;Python具有丰富的第三方库支持,生态丰富,可以支持异步编程和同步编程混合使用。
Python的异步运行时是单线程的,需要经常切换协程,限制了并发能力。同时,对于一些I/O 密集型操作,异步编程模型和同步编程相比,复杂度稍高,难以处理边界和异常情况。
Golang
基于Goroutine和Channel实现Future/Async/Await
执行快、代价小,一个新的Goroutine的栈大约只需要 4-5KB 的内存;Goroutine之间通信方式简单直观,更容易编写程序,代码可读性好;Golang天然支持并发控制,开发分布式系统有很大优势。
Golang缺少异步I/O模型的直接支持,需要自己手工实现异步I/O,开发人员需要了解文件描述符、Socket等内容;Golang的调度器可能会因为竞争条件而导致性能问题,开发人员需要注意保证调度器正确性。
Rust
通过执行器(executor)实现Future/Async/Await
Rust的异步编程模型具有很好的可伸缩性和并发性,Future对象的执行是由执行器自动调度的,可以更好地利用多核CPU;Rust天然地支持异步 I/O 操作,性能更高,更适合高并发场景。
Rust的异步编程模型对于初学者来说较为复杂,需要了解Future、Executor的概念,学习曲线较陡峭。Rust在处理一些复杂场景时,也需要进行代码重构,使得逻辑更加清晰。

异步运行时特点

语言
异步运行时特点
Executor/Reactor
注意
Python
基于协程的异步编程模型,通过asyncio库实现异步运行时
使用事件循环实现Reactor(基于select/epoll实现)
Python的异步运行时是单线程的,限制了其并发能力
Golang
基于Goroutine和Channel实现非阻塞式异步编程
使用调度器实现Executor(基于Go runtime实现)
需要注意Golang的调度器可能会因为竞争条件而导致性能问题
Rust
基于Future和Async/Await实现异步编程模型
使用块级任务调度器实现Executor(基于Tokio实现)
Rust需要对内存安全做出很多保证,导致其在编程复杂度上较高一些

Python、Golang和Rust在异步运行时的实现上有以下不同:

  1. Python使用协程实现异步编程,其异步运行时基于asyncio库。

asyncio库通过事件循环和协程实现异步编程,该库在事件循环中注册了协程,当有IO时,事件循环会自动执行协程。Python的异步运行时是单线程的,通过在事件循环中切换协程实现异步操作。

  1. Golang的异步编程模型是基于并发的原语 Goroutine 和 Channel。 Goroutine是一种轻量级线程,Channel是一种线程间的通信机制,通过Goroutine和Channel可以实现非阻塞的异步编程。Golang的异步运行时是通过调度器(Scheduler)在多个Goroutine之间进行调度。

  2. Rust的异步编程模型是通过Future和Async/Await实现的。 Future是异步计算的结果,而Async函数返回一个Future类型的值,表示异步计算的进程。Rust的异步运行时通过块级任务调度器实现,可以在同一线程上执行,也可以在多个线程上执行。