65.9K
CodeProject 正在变化。 阅读更多。
Home

Julia v1.3 中的新线程功能

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2020年2月19日

CPOL
viewsIcon

15068

Julia 的多线程方法将许多先前已知的想法在一个新颖的框架中结合起来。虽然它们各自独立都有用,但我们相信——正如通常情况一样——整体大于部分之和。

利用多核处理器是任何现代编程语言的关键能力。程序员需要为其应用程序提供最佳吞吐量,因此我们将展示 Julia* v1.3 中的新多线程运行时如何以最小的麻烦释放现代 CPU 的全部威力。

我们考虑的关键因素之一是减轻程序员的负担。Julia 提供了一系列旨在有效组合的现代原语。我们将讨论我们为简化程序员的心智模型所做的一些权衡。Julia 的一个重要设计原则是让常见任务变得容易,让困难任务变得可能。这在语言的多个方面都有体现,例如:

  • 自动内存管理
  • 将函数、对象和模板合并到单一的调度机制中
  • 用于性能的可选类型推断

现在我们将此扩展到并行性。通过构建在语言现有的并发机制之上,我们增加了并行能力,在保留现有代码(相对)简单的单线程执行的同时,允许新代码从多线程执行中受益。这项工作受到了诸如 Threading Building Blocks 等并行编程系统的启发。

在这个范式中,程序的任何一部分都可以被标记为并行执行,并且会启动一个任务以在可用线程上自动运行该代码段。一个动态调度器决定何时何地启动任务。这种并行模型具有许多有用的特性。我们认为它在某种程度上类似于垃圾回收。有了垃圾回收,您可以随意分配对象,而无需担心何时以及如何释放它们。有了任务并行,您可以随意生成任务——可能数百万个——而无需担心它们最终何时何地运行。

该模型是可移植的,并且摆脱了低级细节。程序员不需要管理线程,甚至不需要知道有多少处理器或线程可用。该模型是可嵌套和可组合的。可以启动并行任务,这些任务可以调用库函数,而这些库函数本身又启动并行任务——所有这些都能正确工作。此属性对于大量工作由库函数完成的高级语言至关重要。程序员可以编写串行或并行代码,而无需担心底层库的实现方式。此模型也不仅限于 Julia 库。我们已经证明它可以扩展到原生库,如 FFTW*,并且我们正在努力将其扩展到 OpenBLAS*。

运行带线程的 Julia

让我们看一些使用 Julia v1.3 启动的带多个线程的示例。要在您自己的机器上进行尝试,您需要从 https://julialang. org/downloads 下载最新的 Julia 版本(目前是 v1.3.0)。使用环境变量 JULIA_NUM_THREADS 设置要使用的线程数来运行 ./julia。或者,安装 Julia 后,按照 https://julia-lang.cn/downloads/platform/ 上的步骤安装 Juno IDE*。它将根据可用的处理器核心自动设置线程数。它还提供了一个图形界面来更改线程数。

我们可以通过查询线程数和当前线程的 ID 来验证线程是否正常工作

任务和线程

一种直观的方式来展示线程正在工作,就是观察调度器以半随机、交错的顺序拾取工作。Julia 的先前版本已经有一个 ‘@threads for’ 宏,它会将一个范围分割并在一组线程上运行一部分,采用静态调度。因此,在下面的范围中,线程 1 将运行项目 1 和 2;线程 2 将运行项目 3 和 4;依此类推。

现在可以使用全新的 ‘@spawn’ 宏和现有的 ‘@sync’ 宏来分隔工作项,从而以完全动态的调度运行相同的程序。

现在,让我们看一些更实际的例子。

并行归并排序

经典的归并排序算法在使用多线程时显示出显著的性能提升。此函数将创建 O(n) 个子任务,这些任务将对数组的独立部分进行排序,然后再将它们合并成输入的一个最终排序副本。我们在此使用了每个任务通过 fetch 返回值的能力。



图 1 显示了增加线程数如何影响扩展。由于我们使用的是进程内线程,我们可以通过就地修改输入并重用工作缓冲区来进一步优化,以获得额外的性能。

1 - psort 在具有 40 个超线程的服务器上的扩展比(两颗 Intel® Xeon® Silver 4114 处理器 @ 2.20GHz)

虽然此处未演示,但 fetch 也会自动传播来自子任务的异常。

并行前缀

前缀和(也称为“扫描和”)是另一个可以通过多线程获得良好收益的经典问题。该算法的并行版本通过将工作组织成两个树来计算部分和。下面简短的实现可以利用机器上所有可用的核心和 SIMD 单元。

图 2 显示了增加线程数如何影响扩展。

Julia 的几项功能结合在一起,使得表达这种简单而高性能的实现特别容易。在底层,系统会自动为针对不同类型参数优化的函数版本进行编译。编译器还可以为特定的 CPU 模型自动特化函数,包括提前(ahead-of-time)和即时(just-in-time)编译。Julia 随附了一个“系统映像”,其中包含为合理范围内的 CPU 预编译的代码,但如果运行时使用的处理器支持更广泛的功能集,编译器将自动生成更定制的代码。与此同时,线程系统通过动态调度工作来适应可用的核心。

2 - prefix_threads 在具有 40 个超线程的服务器上的扩展比(两颗 Intel® Xeon® Silver 4114 处理器 @ 2.20GHz)

并行感知 API

应用程序代码中的许多操作必须是线程感知的,才能在并行环境中安全使用。这些面向用户的 API 包括:

  • 并发基础知识Task 以及相关的函数,包括 scheduleyieldwait
  • 互斥锁ReentrantLockCondition 变量,包括 lockunlockwait
  • 同步原语ChannelEventAsyncEventSemaphore
  • I/O 和其他阻塞操作:包括 readwriteopenclosesleep
  • 实验性 Threads 模块:各种构建块和原子操作。

调度器设计

partr 调度器的原型实现最初由 Kiran Pamnany 于 2016 年底在 Intel 工作期间用 C 语言编写,这得益于他对缓存高效调度* 的研究。这项工作的目标是使线程库与全局深度优先工作排序可组合。partr 使用近似优先队列来实现这一点,其中优先级设置为启动任务的线程的线程 ID。

外部库

这项工作的一个重要动机是,我们希望更好地支持多线程库,避免 CPU 过度订阅导致的性能下降(由于缓存抖动和频繁的上下文切换)。以前,唯一的选择是让用户预先决定将 Julia 限制为 N 个线程,并告知线程库(如 libfftw 或 libblas)使用 M ÷ N 个核心。最常见的选择可能是 1 和 M,因此只有应用程序的一部分可以从多个核心中受益。然而,鉴于我们能够快速创建和运行线程池中的工作项,我们正在研究如何让外部库与我们的线程池集成。这是一个持续探索的领域,因为我们正在收集各种库的性能和 API 需求反馈。

我们已成功将 FFTW 适配到我们的线程运行时之上运行,而不是使用其自己的(基于 Pthreads 的工作池)。这只花了我们几个小时。(我们很幸运能够得到该库作者的帮助。)在没有任何性能调优的情况下(目前),它取得了具有竞争力的性能结果。我们学到了优化调度器延迟的重要性,这目前是实现精确性能匹配的持续工作。然而,即使存在一些由通用性带来的开销,我们也期望能够组合线程感知的用户并从 partr 调度器获得资源共享的能力,这将使程序运行整体得到改善。

展望未来

Julia 的多线程方法将许多先前已知的想法在一个新颖的框架中结合起来。虽然它们各自独立都有用,但我们相信——正如通常情况一样——整体大于部分之和。从这一点出发,我们希望看到 Julia 生态系统中涌现出一系列可组合的并行库。

参考文献

  1. https://github.com/kpamnany/partr
  2. Shimin Chen, Phillip B. Gibbons, Michael Kozuch, Vasileios Liaskovitis, Anastassia Ailamaki, Guy E. Blelloch, Babak Falsafi, Limor Fix, Nikos Hardavellas, Todd C. Mowry, and Chris Wilkerson. Scheduling Threads for Constructive Cache Sharing on cmps. In Proceedings of the Nineteenth Annual ACM Symposium on Parallel Algorithms and Architectures, SPAA ’07, pages 105–115, New York, NY, USA, 2007. ACM.
© . All rights reserved.