异步核心
executor 还是 reactor?
- executor: 在 Rust 中,调度 Future 对象的是一个 Executor 线程,它会不停地轮询 Future 的状态,一旦 Future 对象的状态由 “就绪” 变为 “完成”,Executor 就会把结果返回给调用方。异步计算需要包含可中止、存储当前状态和恢复状态这些特征,这些特征都是由 Future trait 统一实现的,这样在使用时避免了许多重复性的工作。Future trait 的 poll 方法会返回一个 Poll 枚举,它包含两个成员:Ready 和 Pending。
- Reactor: 在 Reactor 模型中,主线程通过事件循环来监听异步事件,并在事件发生时调用相应的回调函数。和任务执行不同,整个异步过程是由事件驱动的,主线程会等待事件发生,并且不断轮询异步事件队列。当异步操作完成并返回结果时,主线程会在回调函数中处理结果。Reactror 模型的一个关键点是将事件相关联的回调函数保存在异步队列中,当事件被触发时,会根据事件类型将对应的回调函数压入事件循环队列,然后一次性地调用所有的回调函数。这样做的好处是可以减少线程切换的开销,但也会引入一些问题,比如回调函数的执行时间过长, 会导致其他回调函数的执行被阻塞。为了解决这个问题,可以将回调函数的执行放到线程池中,这样就可以避免阻塞主线程。
Executor 模式和 Reactor 模式本质上都是为了实现异步编程而设计的模式,它们使用不同的机制来处理异步调用。尽管都涉及多线程,但它们的实现方式有很大的不同,不应简单地认为是多线程的组合。
Executor 模式是采用线程池的方式管理多个子线程来执行异步任务,并在主线程上等待所有的任务完成。在 Rust 的 Executor 模式中,主线程通过轮询 Future 对象的方式等待异步任务完成,并不涉及多线程的处理,I/O 操作使用了操作系统提供的异步调用方式,通过信号通知主线程任务完成。在其他编程语言中,Executor 模式可能需要使用多个线程来处理任务执行。
而 Reactor 模式采用了单线程的方式,在主线程上监听异步事件以及相应的回调函数,并在事件触发后调用回调函数来处理异步任务。Reactror 模式的优势在于不会引入线程切换的开销,当然也存在处理IO密集型任务的性能瓶颈,但异步执行的优势在于任务不需要等待,可以使用事件驱动时间进行并行处理。
所以,Executor 模式和 Reactor 模式是两种不同的异步编程模型,它们在实现方面有明显的不同。虽然它们都涉及多线程,但其中 Executor 模式使用线程池管理多个异步任务,而 Reactor 模式使用事件循环监听异步事件并调用相应的回调函数,具体实现上不尽相同。
你只管调用,与系统交互交给异步运行时
在 Rust 中,异步运行时是一个独立的运行时环境,它负责管理异步任务的执行,包括任务的调度、线程池的管理、任务的执行等。
Rust 的异步运行时并没有直接暴露出创建底层线程的接口,而是提供了一些抽象接口和默认实现,以方便用户创建和管理异步任务。如果需要深入了解 Rust 异步运行时的具体实现方式,可以参考 tokio 等库的源码。
在 tokio 的运行时中,线程的创建和销毁、任务的调度和执行都是在 runtime 自己的线程池中完成的。具体流程如下:
- 创建 runtime:在 Builder::new() 方法中调用一系列方法,比如 num_threads()、threaded_scheduler(),以设置线程池大小和线程调度器类型等属性。最终通过 build() 方法创建 runtime 对象。
- 获取 handle:通过 handle() 方法获取 runtime handle,用于在运行时中 spawn 新的 future 和执行异步操作。
- Spawn future:通过 spawn() 方法异步地启动一个 future,并返回一个 JoinHandle,用于跟踪 future 的执行状态。此时 future 并不会被直接执行,而是进入到待执行队列中。
- Run tasks:在 block_on() 方法中,runtime 会持续地从待执行队列中取出 future 并执行,直到所有任务都完成为止。
- Shutdown runtime:通过 drop(rt) 方法关闭 runtime,释放所有资源,包括线程池、任务队列、调度器等。
异步运行时与系统交互
下面是一个概括性的异步运行时如何与系统底层交互的流程:
-
创建运行时对象:在运行时初始化过程中,需要创建并初始化一些系统资源,比如文件描述符、线程池、事件循环等。在这个过程中,需要调用系统底层接口来申请和释放这些资源,并设置相应的事件处理器(比如 epoll 或 kqueue)。
-
注册/注销事件:在执行异步任务的过程中,需要对文件 IO 或网络 IO 等事件进行处理。为了实现高效地异步 IO,可以使用系统底层提供的事件处理器来监听文件描述符和网络端口等事件,并在事件发生时及时通知异步运行时。这个过程需要调用系统底层标准库方法,比如 epoll_ctl 等。
-
调用异步模块:当程序使用异步模块来发起异步操作请求时,通常是将请求的状态信息(比如回调函数、数据缓冲区等)传递给异步运行时。异步运行时会将这些状态信息保存到一个任务队列中,以便在事件就绪后及时调用相应的回调函数进行异步操作。
-
处理异步事件:当异步请求的 IO 事件发生时,事件处理器会向异步运行时发送通知,并触发异步事件处理函数的执行。在这个过程中,异步运行时需要调用系统底层提供的标准库方法,比如 poll 等,来获取异步 IO 事件的状态和数据,并将其传递给相应的回调函数。如果异步请求执行完毕,异步运行时也需要从任务队列中移除对应的状态信息,以释放底层资源并回收内存。
对比python和golang
future对比
异步运行时特点
Python、Golang和Rust在异步运行时的实现上有以下不同:
- Python使用协程实现异步编程,其异步运行时基于asyncio库。
asyncio库通过事件循环和协程实现异步编程,该库在事件循环中注册了协程,当有IO时,事件循环会自动执行协程。Python的异步运行时是单线程的,通过在事件循环中切换协程实现异步操作。
-
Golang的异步编程模型是基于并发的原语 Goroutine 和 Channel。 Goroutine是一种轻量级线程,Channel是一种线程间的通信机制,通过Goroutine和Channel可以实现非阻塞的异步编程。Golang的异步运行时是通过调度器(Scheduler)在多个Goroutine之间进行调度。
-
Rust的异步编程模型是通过Future和Async/Await实现的。 Future是异步计算的结果,而Async函数返回一个Future类型的值,表示异步计算的进程。Rust的异步运行时通过块级任务调度器实现,可以在同一线程上执行,也可以在多个线程上执行。