展开描述
Tokio 运行时。
与其他 Rust 程序不同,异步应用程序需要运行时支持。特别是,需要以下运行时服务:
- 一个 I/O 事件循环(亦称 driver 驱动程序),它驱动 I/O 资源,并将 I/O 事件分发给依赖它们的 task。
- 一个 调度器,用于执行使用这些 I/O 资源的 tasks。
- 一个 定时器,用于在一段时间后调度工作运行。
Tokio 的 Runtime 将所有这些服务捆绑为单一类型,允许它们一起启动、关闭和配置。然而,通常不需要手动配置 Runtime,用户可以直接使用 tokio::main 属性宏,它会在底层创建一个 Runtime。
§选择运行时
以下是为你的应用程序选择合适运行时的经验法则。
+------------------------------------------------------+
| 你需要工作窃取或多线程调度器吗? |
+------------------------------------------------------+
| 是 | 否
| |
| |
v |
+------------------------+ |
| 多线程 Runtime | |
+------------------------+ |
|
V
+--------------------------------+
| 你执行的是 `!Send` Future 吗? |
+--------------------------------+
| 是 | 否
| |
V |
+---------------+ |
| 本地 Runtime | |
+---------------+ |
|
v
+------------------------+
| 当前线程 Runtime |
+------------------------+上述决策树并不详尽。还有其他因素可能会影响你的决策。
§Bridging with sync code
有关详细信息,请参阅 https://tokio.rs/tokio/topics/bridging。
§NUMA awareness
tokio 运行时不支持 NUMA(非一致性内存访问)。在 NUMA 系统上,为了获得更好的性能,你可能希望启动多个运行时而不是单个运行时。
§Usage
当不需要微调时,可以使用 tokio::main 属性宏。
use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
loop {
let (mut socket, _) = listener.accept().await?;
tokio::spawn(async move {
let mut buf = [0; 1024];
// In a loop, read data from the socket and write the data back.
loop {
let n = match socket.read(&mut buf).await {
// socket closed
Ok(0) => return,
Ok(n) => n,
Err(e) => {
println!("failed to read from socket; err = {:?}", e);
return;
}
};
// Write the data back
if let Err(e) = socket.write_all(&buf[0..n]).await {
println!("failed to write to socket; err = {:?}", e);
return;
}
}
});
}
}在运行时上下文中,可以使用 tokio::spawn 函数生成其他任务。使用此函数生成的 future 将在 Runtime 使用的同一线程池上执行。
也可以直接使用 Runtime 实例。
use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::runtime::Runtime;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create the runtime
let rt = Runtime::new()?;
// Spawn the root task
rt.block_on(async {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
loop {
let (mut socket, _) = listener.accept().await?;
tokio::spawn(async move {
let mut buf = [0; 1024];
// In a loop, read data from the socket and write the data back.
loop {
let n = match socket.read(&mut buf).await {
// socket closed
Ok(0) => return,
Ok(n) => n,
Err(e) => {
println!("failed to read from socket; err = {:?}", e);
return;
}
};
// Write the data back
if let Err(e) = socket.write_all(&buf[0..n]).await {
println!("failed to write to socket; err = {:?}", e);
return;
}
}
});
}
})
}§Runtime Configurations
Tokio 提供多种任务调度策略,适用于不同的应用程序。运行时构建器或 #[tokio::main] 属性可用于选择要使用的调度器。
§Multi-Thread Scheduler
多线程调度器在线程池上使用工作窃取策略执行 future。默认情况下,它会为系统上每个可用的 CPU 核心启动一个工作线程。这往往是大多数应用程序的理想配置。多线程调度器需要 rt-multi-thread 特性标志,并且默认选中:
use tokio::runtime;
let threaded_rt = runtime::Runtime::new()?;除了一些小众用例(例如只需要运行单个线程时),大多数应用程序应使用多线程调度器。
§Current-Thread Scheduler
当前线程调度器提供一个单线程的 future 执行器。所有任务都将创建并在当前线程上执行。这需要 rt 特性标志。
use tokio::runtime;
let rt = runtime::Builder::new_current_thread()
.build()?;§Resource drivers
手动配置运行时时,默认不启用任何资源驱动程序。在这种情况下,尝试使用网络类型或时间类型将失败。为了启用这些类型,必须启用资源驱动程序。这是通过 Builder::enable_io 和 Builder::enable_time 完成的。作为简写,Builder::enable_all 启用这两个资源驱动程序。
§Driving the runtime
Tokio 运行时仅在运行时正在运行时才能执行任务。通常这不是问题,因为运行时的默认配置始终在运行,但是像当前线程运行时之类的替代配置要求必须调用 Runtime::block_on。
- A multi-threaded runtime is always running because it spawns its own worker threads.
- A current-thread runtime does not spawn any worker threads, so it can only
execute tasks when you provide a thread by calling
Runtime::block_on. - A
LocalSetonly executes local tasks spawned on it when theLocalSetis.awaitedor otherwise driven using one of its methods for this purpose.
请注意,Handle::block_on 不会驱动运行时。在使用当前线程运行时时,必须至少调用一次 Runtime::block_on。仅调用 Handle::block_on 是不够的。
§Lifetime of spawned threads
运行时可能会根据其配置和使用情况生成线程。多线程调度器生成线程以调度任务和用于 spawn_blocking 调用。
当 Runtime 处于活动状态时,线程可能会在空闲一段时间后关闭。一旦 Runtime 被 drop,所有运行时线程通常已被终止,但是在存在无法停止的已生成工作的情况下,不能保证它们已被终止。有关更多详细信息,请参阅结构体级别文档。
§Detailed runtime behavior
本节详细介绍了 Tokio 运行时将如何调度任务以供执行。
在最基本层面上,运行时具有一组需要调度的任务。它会重复地从该集合中移除一个任务并调度它(通过调用 poll)。当集合为空时,线程将进入睡眠状态,直到有新任务添加到集合中。
然而,仅有以上这些不足以保证运行时的良好行为。例如,运行时可能有一个始终准备好被调度的任务,并且每次都调度该任务。这是一个问题,因为它不调度其他任务会导致它们挨饿。为了解决这个问题,Tokio 提供了以下公平性保证:
如果任务总数不会无限制地增长,并且没有任务阻塞线程,那么可以保证任务被公平地调度。
或者,更正式地讲:
在以下两个假设下:
- There is some number
MAX_TASKSsuch that the total number of tasks on the runtime at any specific point in time never exceedsMAX_TASKS.- There is some number
MAX_SCHEDULEsuch that callingpollon any task spawned on the runtime returns withinMAX_SCHEDULEtime units.那么,存在某个数字
MAX_DELAY,使得当一个任务被唤醒时,它将在MAX_DELAY个时间单位内被运行时调度。
(这里,MAX_TASKS 和 MAX_SCHEDULE 可以是任何数字,运行时的用户可以选择它们。MAX_DELAY 数字由运行时控制,取决于 MAX_TASKS 和 MAX_SCHEDULE 的值。)
除了上述公平性保证外,不保证任务调度的顺序。也不保证运行时对所有任务同样公平。例如,如果运行时有两个任务 A 和 B 都已就绪,那么运行时可能会先调度 A 五次,然后再调度 B。即使 A 使用 yield_now 屈服,也是如此。所保证的只是它最终会调度 B。
通常,仅当任务已通过对 waker 调用 wake 被唤醒时才会被调度。但是,这并不是保证的,在某些情况下,Tokio 可能会调度尚未被唤醒的任务。这称为伪唤醒。
§IO and timers
除了调度任务之外,运行时还必须管理 IO 资源和计时器。它通过定期检查是否有任何就绪的 IO 资源或计时器,并唤醒相关任务以使其被调度,来实现这一点。
这些检查在调度任务之间定期执行。在与之前公平性保证相同的假设下,Tokio 保证它将在某个最大时间单位数内唤醒具有 IO 或计时器事件的任务。
§Current thread runtime (behavior at the time of writing)
本节描述了 当前线程运行时 当前的行为方式。此行为可能会在未来的 Tokio 版本中发生变化。
当前线程运行时维护两个准备调度的任务的 FIFO 队列:全局队列和本地队列。运行时将优先从本地队列中选择要调度的下一个任务,并且仅在本地队列为空时或已连续从本地队列中选择任务 31 次时,才从全局队列中选择任务。数字 31 可以使用 global_queue_interval 设置进行更改。
每当没有任务可以被调度时,或者已经连续调度了 61 个任务时,运行时将检查新的 I/O 或定时器事件。数字 61 可以使用 event_interval 设置进行更改。
当任务是从运行时上运行的任务内部被唤醒时,则唤醒的任务被直接添加到本地队列。否则,该任务被添加到全局队列。当前线程运行时不使用lifo 槽优化。
§Multi threaded runtime (behavior at the time of writing)
本节描述了 多线程运行时 当前的行为方式。此行为可能会在未来的 Tokio 版本中发生变化。
多线程运行时具有固定数量的工作线程,所有这些线程都是在启动时创建的。多线程运行时维护一个全局队列,以及每个工作线程的一个本地队列。一个工作线程的本地队列最多可以容纳 256 个任务。如果添加到本地队列的任务超过 256 个,则其中的一半将被移动到全局队列以腾出空间。
运行时将优先从本地队列中选择下一个要调度的任务,并且仅当本地队列为空,或已连续从本地队列中选择了 global_queue_interval 次任务时,才会从全局队列中选择任务。如果未使用运行时 builder 显式设置 global_queue_interval 的值,则运行时将使用启发式方法动态计算它,该方法以每次检查全局队列之间间隔 10 毫秒为目标(基于 worker_mean_poll_time 指标)。
如果本地队列和全局队列都为空,那么工作线程将尝试从其他工作线程的本地队列窃取任务。窃取是通过将一个本地队列中的一半任务移动到另一个本地队列来完成的。
每当没有任务可以被调度时,或者已经连续调度了 61 个任务时,运行时将检查新的 I/O 或定时器事件。数字 61 可以使用 event_interval 设置进行更改。
多线程运行时使用 lifo slot 优化:每当一个任务唤醒另一个任务时,另一个任务将添加到工作线程的 lifo slot 中,而不是添加到队列中。如果在发生这种情况时 lifo slot 中已存在任务,则替换 lifo slot,并且原来在 lifo slot 中的任务将放入线程的本地队列中。当运行时完成一个任务的调度时,如果有 lifo slot,它将立即调度 lifo slot 中的任务。使用 lifo slot 时,coop budget 不会重置。此外,如果工作线程连续三次使用 lifo slot,则会暂时禁用它,直到工作线程调度了一个不是来自 lifo slot 的任务。可以使用 disable_lifo_slot 设置禁用 lifo slot。lifo slot 与本地队列分开,因此其他工作线程无法窃取 lifo slot 中的任务。
当任务是从非工作线程唤醒时,则该任务被放入全局队列中。
§Performance tuning
§File descriptor table pre-warming
在 Linux 上,文件描述符表的增长可能会阻塞工作线程。请参阅 prewarm-fd-table 示例。
结构体§
- Builder
- Builds Tokio Runtime with custom configuration values.
- Enter
Guard - Runtime context guard.
- Handle
- Handle to the runtime.
- Id
- An opaque ID that uniquely identifies a runtime relative to all other currently running runtimes.
- Local
Options LocalRuntime-only config options- Local
Runtime - A local Tokio runtime.
- Runtime
- The Tokio runtime.
- Runtime
Metrics - Handle to the runtime’s metrics.
- TryCurrent
Error - Error returned by
try_currentwhen no Runtime has been started
枚举§
- Runtime
Flavor - The flavor of a
Runtime.
函数§
- is_
rt_ shutdown_ err - Checks whether the given error was emitted by Tokio when shutting down its runtime.