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

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

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

协程或者绿色线程是近年来经常讨论的话题。Tokio作为Rust上协程调度器实现的典型代表,其设计和实现都有其特色。本文是Tokio团队在新版本调度器发布后,对其设计和实现的经验做的总结,十分值得一读。

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

Tokio——作为 Rust 语言的异步运行时,我们一直在为它的下一个大版本发布而努力。今天,伴随着 Pull request 的提交这个成果终于可以呈现出来:一个完全重写的调度器,带来巨大的性能提升。在一些性能基准测试中表现出10倍的提升,此外我们也对一些容易受到影响的“用例”,比如 Hyper 及 Tonic,做了额外的测试,以验证新的调度器是否如预期表现。(当然我们可以提前剧透下:结果非常棒!)

在我们着手之前,我花了大量时间去寻找其他可参考的调度器实现及其他信息,但是基本上除了(代码)实现本身,并没有发现太多有用的资料。同时我还发现,现有的大部分调度器实现,代码晦涩难懂。所以在新版 Tokio 的实现过程中,我始终提醒自己确保代码实现易读易懂。之所以写这篇关于调度器实现的详细文章,也是希望能帮助到其他人。

本文会从调度器的设计展开,然后再围绕新版调度器的一些特定细节。包括以下一些部分:

  • 新的 std:future任务系统(task system)
  • 更好的队列算法
  • 如何优化消息传递模式
  • 改进的“任务窃取”算法(throttle-stealing)
  • 减少跨线程同步
  • 减少内存分配
  • 减少原子的引用计数

可以看出来新的设计实现是围绕“减法”,有句话说:“没有什么代码比无代码更快”,话糙理不糙。

本文还覆盖了我们如何去测试新的调度器,我们都知道设计和实现出正确的、无锁的、并发编程是非常困难和有挑战的。毕竟,慢总好过有缺陷,特别是和内存安全有关的缺陷。所以我们为新调度器还设计开发了一个叫做 loom 的并发测试工具。

接下来,我建议读者们可以接杯咖啡,把座椅调整舒服,这将是一篇很长但需要集中注意力的文章。

调度器是如何工作的?

调度器,顾名思义,就是如何调度程序执行。通常来说,程序会分成许多“工作单元”,我们将这种工作单元成为任务(task)。一个任务要么是可运行的,要么是挂起的(空闲的或阻塞的)。任务是彼此独立的,因为处在“可运行的”任务都可能被并发的执行。调度器的职责就是执行任务,直到任务被挂起。这个过程中隐含得本质就是如何为任务分配全局资源——CPU 时间。

接下来的内容里只是围绕“用户空间”的调度器,有操作系统基础知识的读者应该明白,指的是运行于操作系统线程之上的调度器,而操作系统线程则是由内核调度器所调度。

Tokio 调度器会执行 Rust 的 future,就像我们讨论 Java 语言、Go 语言等线程模型时一样,Rust 的 future可以理解为 Rust 语言的“异步绿色线程”,它是 M:N 模式,很多用户空间的任务通过多路复用跑在少量的系统线程上。

调度器的设计模式有很多种,每种都有各自的优缺点。但本质上,可以将调度器抽象得看作是一个(任务)队列,以及一个不断消费队列中任务的处理器,我们可以用伪代码表示成如下形式:

  1. while let Some(task) = self.queue.pop { task.run;} 

当任务变成“可运行”的,就被插入到队列中:

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

虽然我们可以设计成将资源、任务以及处理器都存在于一个单独线程中,但 Tokio 还是选择多线程模型。现代计算机都具有多个 CPU 以及多个物理核,使用单线程模型调度器会严重得限制资源利用率,所以为了尽可能压榨所有 CPU 或物理核的能力,就需要:

  • 单一全局的任务队列,多处理器
  • 多任务队列,每个都有单独的处理器

单队列+多处理器

这种模型中,有一个全局的任务队列。当任务处于“可运行的”状态时,它被插到任务队列尾。处理器们都在不同的线程上运行,每个处理器都从队列头取出任务并“消费”,如果队列为空了,那所有线程(以及对应的处理器)都被阻塞。

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

任务队列必须支持多个生产者和多个消费者。这里常用的算法就是使用侵入式链表,这里的侵入式表示放入队列的任务本身需要包含指向下(后)一个任务的指针。这样在插入和弹出操作时就可以避免内存分配的操作,同时插入操作是无锁,但弹出操作就需要一个信号量去协调多个消费者。

这种方式多用于实现通用线程池场景,它具有如下的优点:

  • 任务会被公平地调度
  • 实现相对简单明了

上面说得公平调度意味着所有任务是以“先进先出”的方式来调度。这样的方式在有一些场景下,比如使用 fork-join 方式的并行计算场景时就不够高效了。因为唯一重要的考量是最终结果的计算速度,而非子任务的公平性。

当然这种调度模型也有缺点。所有的处理器(消费者)都守着队列头,导致处理器真正执行任务所消耗的时间远远大于任务从队列中弹出的时间,这在长耗时型任务场景中是有益的,因为队列争用会降低。然而,Rust 的异步任务是被设计用于短耗时的,此时争用队列的开销就变得很大了。

并发和“机械共情”

读者们肯定听过“为xxx平台特别优化”这样的表达,这是因为只有充分了解硬件架构,才能知道如何最大化利用硬件资源,才能设计出运行性能最高的程序。这就是所谓的“机械共情”,这个词是由马丁汤普森最初提出并使用的。

至于现代硬件架构下如何处理并发的相关细节并不在本文讨论的范围内,感兴趣的读者也可以阅读文章末的更多参考资料部分。

(编辑:核心网)

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

热点阅读