加入收藏 | 设为首页 | 会员中心 | 我要投稿 核心网 (https://www.hxwgxz.com/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 建站 > 正文

硬核!Rust异步编程方式重大升级:新版Tokio如何提升10倍性能详解

发布时间:2019-10-24 04:19:00 所属栏目:建站 来源:高可用架构
导读:协程或者绿色线程是近年来经常讨论的话题。Tokio作为Rust上协程调度器实现的典型代表,其设计和实现都有其特色。本文是Tokio团队在新版本调度器发布后,对其设计和实现的经验做的总结,十分值得一读。 Tokio作为 Rust 语言的异步运行时,我们一直在为它的

最后一部分是全局队列。该队列用于处理本地队列的溢出,以及从非处理器线程向调度程序提交任务。如果处理器有负载,即本地队列中有任务。在从本地队列执行约60个任务后,处理器将尝试从全局队列获取任务。当处于“搜索”状态时,它还会检查全局队列,如下所述。

优化消息传递模式

用Tokio编写的应用程序通常以许多小的独立任务为模型。这些任务将使用消息相互通信。这种模式类似于Go和Erlang等其他语言。考虑到这种模式的普遍性,调度程序尝试对其进行优化是有意义的。

给定任务A和任务B。任务A当前正在执行,并通过channel向任务B发送消息。通道是任务B当前阻塞在channel上,因此发送消息将导致任务B转换为可运行状态,并被入队到当前处理器的运行队列中。然后,处理器将从运行队列中弹出下一个任务,执行该任务,然后重复执行直到完成任务B。

问题在于,从发送消息到执行任务B的时间之间可能会有很大的延迟。此外,“热”数据(例如消息)在发送时已存储在CPU高速缓存中,但是到任务B被调度时,有可能已经从高速缓存中清理掉了。

为了解决这个问题,新的Tokio调度程序实现了特定优化(也可以在Go和Kotlin的调度程序中找到)。当任务转换为可运行状态时,它存储在“下一个任务”槽中,而不是将其入队到队列的后面。在检查运行队列之前,处理器将始终检查该槽。将任务插入此槽时,如果任务已存储在其中,则旧任务将从槽中移除,并入队到队列的后面。在消息传递的情况下,这将保证消息的接收者会被立马调度。

硬核!Rust异步编程方式重大升级:新版Tokio如何提升10倍性能详解

任务窃取

在窃取任务调度器中,当处理器的运行队列为空时,处理器将尝试从同级处理器中窃取任务。随机选择同级处理器,然后对该同级处理器执行窃取操作。如果未找到任务,则尝试下一个同级处理器,依此类推,直到找到任务。

实际上,许多处理器通常在大约同一时间完成其运行队列的处理。当一批任务到达时(例如,轮询epoll以确保socket就绪时),就会发生这种情况。处理器被唤醒,获取并运行任务。这导致所有处理器同时尝试窃取,意味着多线程试图访问相同的队列。这会引起争用。随机选择初始节点有助于减少争用,但是仍然很糟糕。

新的调度程序会限制并发执行窃取操作的处理器的数量。我们将试图窃取的处理器状态称为“正在搜索任务”,或简称为“正在搜索”状态。通过使用原子计数保证处理器在开始搜索之前递增以及在退出搜索状态时递减来控制并发数量。搜索程序的最大数量是处理器总数的一半。虽然限制相当草率,但依然可以工作。我们对搜索程序的数量没有硬性限制,只需要节流即可,以精度来换取算法效率。

处于正在搜索状态后,处理器将尝试从同级任务线程中窃取任务并检查全局队列。

减少跨线程同步

任务窃取调度程序的另一个关键部分是同级通知。这是处理器在观察新任务时通知同级的地方。如果其他处理器正处于休眠状态,则被唤醒并窃取任务。通知还有另一个重要责任。回顾使用弱原子顺序(获取/发布)的队列算法。由于原子内存顺序的工作原理,而无需额外的同步,因此无法保证同级处理器将知道队列中的任务被窃取。因此通知动作还负责为同级处理器建立必要的同步,以使其知道任务以窃取任务。这些要求使得通知操作代价高昂。我们的目标是在保证CPU利用率的情况下,尽可能少地执行通知操作。通知太多会导致惊群问题。

老版本的Tokio调度程序采用了朴素的通知方式。每当将新任务推送到运行队列时,就会通知处理器。每当该处理器并在唤醒时找到任务,它便会通知另一个处理器。这种逻辑会导致所有处理器都被唤醒从而引起争用。通常这些处理器中的大多数都找不到任务,然后重新进入休眠。

通过使用Go调度器中类似的技术,新调度器有显着改进。新调度器在相同的地方进行执行,然而仅在没有处于搜索状态的worker时才进行通知。通知worker后,其立即转换为搜索状态。当处于搜索状态的处理器找到新任务时,它会首先退出搜索状态,然后通知下一个处理器。

这种方法用于限制处理器唤醒的速率。如果一次调度了一批任务(例如,在轮询epoll以确保套接字就绪时),则处理器会收到第一个任务的通知,然后处于搜索状态。该处理器不会收到批处理中的其余任务的通知。负责通知的处理程序将窃取批处理中的一半任务,然后通知另一个处理器。第三个处理器将被唤醒,从前两个处理器中查找任务,然后窃取其中一半。这样处理器负载会平滑上升,任务也会达到快速负载平衡。

减少内存分配

新的Tokio调度程序对每个任务只需要分配一次内存,而旧的调度程序则需要分配两次内存。以前,Task结构如下:

  1. struct Task { /// All state needed to manage the task state: TaskState, 
  2.  /// The logic to run is represented as a future trait object. future: Box<dyn Future<Output = >>,} 

然后,Task结构也将被置于Box中。自从旧的Tokio调度程序发布以来,发生了两件事。首先,std :: alloc稳定了。其次,Future任务系统切换到显式的vtable策略。有了这两个条件,我们就可以减少一次内存分配。

(编辑:核心网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

热点阅读