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

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

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

通常来说,硬件不是通过提高速度(频率)而是为程序提供更多的 CPU 核来获取性能提升。每个核都可以在极短的时间内执行大量的计算,相较而言,访问内存之类的操作则需要更多时间。因此,为了使程序运行得更快,我们必须使每次内存访问的 CPU 指令数量最大化。尽管编译器可以帮助我们做很多事,但作为程序设计开发人员,我们需要谨慎地考虑数据在内存中的结构布局以及访问内存的模式。

当涉及到线程并发时,CPU 的缓存一致性机制就会发挥作用,它会确保每个 CPU 的缓存都保持最新状态。

所以显而易见,我们要尽可能地避免跨线程同步,因为它是性能杀手。

多处理器+多任务队列

与前面的模型对比,在这种方式下,我们使用多个单线程调度器,每个处理器都有自己独占的任务队列,这样完全避免了同步问题。由于 Rust 的任务模型要求任意线程都可以提交任务到队列,所以我们仍需要设计一种线程安全的方式。要么每个处理器的任务队列支持线程安全的插入操作(MPSC),要么就每个处理器有两个队列:非同步队列和线程安全队列。

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

这便是 Seastar 所使用的策略。因为几乎完全避免了同步,所以性能非常高。但需要注意的是,这并不是灵丹妙药,因为无法确保任务负载都是完全一致统一的,处理器可能出现严重的负载不均衡,使得资源利用率低下。这通常产生的场景是任务被粘到了固定的、特定的处理器上。

众所周知,真实世界的任务负载并不是一致统一的,所以在设计通用调度器时要避免使用此种模型。

“任务窃取”调度器

通常来说,任务窃取调度器是建立在分片调度模型之上的,主要为了解决资源利用率低的问题。每个处理器都具有自己独占的任务队列,处于“可运行的”任务会被插入到当前处理器的队列中,并且只会被当前处理器所消费(执行)。但巧妙的是,当一个处理器空闲时,它会检查同级的其他处理器的任务队列,看看是不是能“窃取”一些任务来执行。这也是这种模型的名称含义所在。最终,只有在无法从其他处理器的任务队列那里获得任务时该处理器就会进入休眠。

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

这几乎是“两全其美”的方法。处理器可以独立运行,避免了同步开销。而且如果任务负载在处理器间分布不均衡,调度器也能够重新分配负载。正是由于这样的特性,诸如 Go 语言、Erlang 语言、Java 语言等都采用了“任务窃取”调度器。

当然,它也是有缺点的,那就是它的复杂性。任务队列必须支持“窃取”操作,并且需要一些跨处理器同步操作。整个过程如果执行不正确,那“窃取”的开销就超过了模型本身的收益。

让我们来考虑一个场景:处理器 A 当前正在执行任务,并且此刻它的任务队列是空的;处理器 B 此时空闲,它尝试“窃取”任务但是失败了,因此进入休眠态。紧接着,处理器 A 所执行的任务产生出了20个(子)任务。目的是唤醒处理器 B。这进而就需要调度器在观察到任务队列中有新的任务时,向处于休眠态的处理器发出信号。显而易见,这样的场景下会需要额外的同步操作,但这恰恰是我们想要避免的。

综上所述:

  • 尽量减少同步操作总是好的
  • “任务窃取”是通用调度器的首选算法
  • 处理器间基本是相互独立的,但是“偷窃”操作时不可避免的需要一些同步操作

Tokio 0.1 调度器

2018年3月,Tokio 发布了其第一版基于“任务窃取”算法的调度器。但那个版本的实现中有一些瑕疵:

首先,I/O 型任务会被同时操作 I/O 选择器(epoll、kqueue、iocp等)的线程所执行;更多与 CPU 绑定的任务会进入线程池。在这种情况下,活跃态线程的数量应该是灵活的、动态的,所以(适时得)关闭空闲态线程是合理的。但是,在“任务窃取”调度器上执行所有异步任务时,始终保持少量的活跃态线程是更合理的。

其次,当时采用了基于 Chase-Lev deque 算法的队列,该算法后来被证明并不适合于调度独立的异步任务场景。

第三,实现过于复杂。由于代码中过多得使用 atomic,然而大部分情况下,mutex 是更好地选择。

最后,代码中有许多细小的低效设计和实现,但由于早期为保证 API 的稳定性,导致了一些技术债。

当然,随着 Tokio 新版的发布,我们收获了很多的经验教训,偿还了许多技术债,这着实是令人兴奋的!

下一代的 Tokio 调度器

现在我们深入解析一下新调度器的变更。

新的任务系统

首先,重要的亮点并不属于 Tokio 的一部分,但对达成我们的成就至关重要:std 包含了由 Taylor Cramer设计的新的任务系统。该系统给调度系统提供了钩子(hooks),方便调度器执行 Rust 异步任务,并且确实做得很好,比之前的版本更轻巧灵活。

Waker结构由资源保存,用于表示任务可运行并被推送到调度程序的运行队列中。在新的任务系统中,Waker结构过去是更大的,但指针宽度为两个指针。减小大小对于最小化复制Waker值的开销以及在结构中占用较少的空间非常重要,从而允许将更多关键数据放入高速缓存行中。自定义vtable设计可实现许多优化,这将在后面讨论。

更好的任务队列

任务队列是调度程序的核心,是最关键的组成部分。最初的tokio调度器使用crossbeam的deque实现,即单生产者、多消费者deque。任务从一端入队,从另一端出队。大多数情况下,入队线程会出队它,然而,其他线程偶尔会出队任务来“窃取”。deque包含一个数组和一组追踪头部和尾部的索引。当deque满了时,入队数据将导致存储空间增长。会分配一个新的、更大的数组,并将值移到新存储区中。

(编辑:核心网)

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

热点阅读