`Thread.sleep` 与 Java 的 Project Loom 中的虚拟线程(纤程)不同吗

Is `Thread.sleep` different with virtual threads (fibers) in Project Loom for Java

我用Thread.sleep when experimenting or demonstrating Java code for concurrency。通过睡觉,我假装正在进行一些需要一些时间的处理工作。

我想知道在 Project Loom 下做这件事。

为了自学,我观看了 Oracle 的 Ron Pressler 介绍 Project Loom 技术的一些 2020 年底视频 (here, here)。虽然很有启发性,但我不记得他提到过休眠线程的问题。

  • Under Project Loom technology with virtual threads (fibers), can we use Thread.sleep in the same way?

看起来是这样。我参考了 OpenJDK wiki 上地址 blocking operations in Loom 的页面。它列出Thread.sleep()个对虚拟线程友好的操作,这意味着

When not pinned, they will release the underlying carrier thread to do other work when the operation blocks.

你接着问,

  • Is there any thing different or noteworthy about sleeping a virtual thread versus sleeping a platform/kernel thread?

文档很少,不清楚实际存在的任何差异是否是有意为之。不过,我倾向于认为 objective 是为了使虚拟线程休眠以使其语义与休眠普通线程的语义尽可能接近。我怀疑足够聪明的程序会有办法区分,但如果有任何差异上升到“值得注意”的水平,那么我希望它们将被视为错误。我部分基于推理,但我也建议您参考 java.net 上的 State of Loom 文档,其中列出了

的“关键要点”
  • A virtual thread is a Thread — in code, at runtime, in the debugger and in the profiler.

  • No language changes are needed.

(强调已添加。)

查看source code,当您在虚拟线程上调用sleep(...)时,它由JVM 的虚拟线程调度程序处理;即不直接执行系统调用也不阻塞本机线程。

所以:

Under Project Loom technology with virtual threads (fibers), can we use Thread.sleep in the same way?

是的。

Is there any thing different or noteworthy about sleeping a virtual thread versus sleeping a platform/kernel thread?

休眠虚拟线程的处理方式与您期望虚拟线程的行为方式相同。性能将与内核线程不同,但行为被设计为 透明 应用程序代码......不会对线程调度程序行为做出无根据的假设。

无论如何,Loom 中 Thread.sleep(...) 的 javadoc 目前没有提到内核和虚拟线程之间的任何区别。

and the 都是正确的,而且内容丰富。我想我会添加一个代码示例来显示:

  • 虚拟线程和 platform/kernel 线程如何尊重 Thread.sleep
  • Project Loom 技术可能带来惊人的性能提升。

基准代码

我们简单写一个循环吧。在每个循环中,我们实例化一个 Runnable 来执行一个任务,并将该任务提交给一个 executor service. Our task is: do some simple math, subtraction from the long returned by System.nanoTime。最后,我们将该数字打印到控制台。

但诀窍在于计算之前,我们让执行该任务的线程休眠。由于每次休眠的初始时间为 12 秒,我们应该在至少 12 秒的停滞时间之后才能在控制台上看到任何内容。

然后提交的任务执行它们的工作。

我们 运行 这有两种方式,通过 enabling/disabling 一对注释掉的行。

  • ExecutorService executorService = Executors.newFixedThreadPool( 5 )
    常规线程池,在主频为 3 GHz 的 Mac mini (2018) 上使用 6 个真实内核中的 5 个(无超线程)英特尔酷睿 i5 处理器和 32 GB 内存。
  • ExecutorService executorService = Executors.newVirtualThreadExecutor()
    由 Project Loom 提供的新虚拟线程(纤程)支持的执行器服务,在此早期访问的特殊版本中 Java16.
package work.basil.example;

import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TooFast
{
    public static void main ( String[] args )
    {
        TooFast app = new TooFast();
        app.demo();
    }

    private void demo ( )
    {
        System.out.println( "INFO - starting `demo`. " + Instant.now() );

        long start = System.nanoTime();
        try (
                // 5 of 6 real cores, no hyper-threading.
                ExecutorService executorService = Executors.newFixedThreadPool( 5 ) ;
                //ExecutorService executorService = Executors.newVirtualThreadExecutor() ;
        )
        {
            Duration sleep = Duration.ofSeconds( 12 );
            int limit = 100;
            for ( int i = 0 ; i < limit ; i++ )
            {
                executorService.submit(
                        new Runnable()
                        {
                            @Override
                            public void run ( )
                            {
                                try {Thread.sleep( sleep );} catch ( InterruptedException e ) {e.printStackTrace();}
                                long x = ( System.nanoTime() - 42 );
                                System.out.println( "x = " + x );
                            }
                        }
                );
            }
        }
        // With Project Loom, the flow-of-control  blocks here until all submitted tasks have finished.
        Duration demoElapsed = Duration.ofNanos( System.nanoTime() - start );

        System.out.println( "INFO - demo took " + demoElapsed + " ending at " + Instant.now() );
    }
}

结果

结果令人吃惊。

首先,在这两种情况下,我们都看到在任何控制台 activity 之前都有超过 12 秒的延迟。所以我们知道 Thread.sleep 是由 platform/kernel 线程和虚拟线程真正执行的。

其次,虚拟线程完成所有任务仅需几秒钟,而传统线程只需几分钟、几小时或几天。

有 100 个任务:

  • 常规线程需要 4 分钟 (PT4M0.079402569S)。
  • 虚拟线程只需 12 秒多一点 (PT12.087101159S)。

有 1,000 个任务:

  • 常规线程需要 40 分钟 (PT40M0.667724055S)。
    (这是有道理的:1,000 * 12 / 5 / 60 = 40 )
  • 虚拟线程需要 12 秒 (PT12.177761325S)。

有 1,000,000 个任务:

  • 常规线程需要……好吧,几天。
    (我实际上并没有等待。我之前在这个的早期版本中经历过 29 小时 运行 的 50 万个循环代码。)
  • 虚拟线程需要 28 秒 (PT28.043056938S)。
    (如果我们减去 12 秒的休眠死时间,则在剩余的 16 秒内执行所有工作的一百万个线程大约为每秒立即执行 62,500 个线程任务。)

结论

使用常规线程,我们可以看到控制台上突然出现几行的重复爆发。所以我们可以看到 platform/kernel 线程实际上是如何在核心上阻塞的,因为它们等待 12 秒 Thread.sleep 到期。然后所有五个线程大约在同一时刻醒来,大约在同一时刻开始,每 12 秒,同时进行计算并写入控制台。由于我们在 Activity Monitor 应用程序中看到 CPU 内核很少使用,因此确认了此行为。

顺便说一句:我假设主机 OS 注意到我们的 Java 线程实际上忙于无所事事,然后使用其 CPU 调度程序暂停我们的 Java 个线程被阻塞时,让其他进程(例如其他应用程序)使用 CPU 个内核。但如果是这样,这对我们的 JVM 来说是透明的。从 JVM 的角度来看,休眠 Java 个线程在整个午睡期间占用 CPU。

对于虚拟线程,我们看到了截然不同的行为。 Project Loom 的设计使得当一个虚拟线程阻塞时,JVM 将该虚拟线程从 platform/kernel 线程中移出,并放置另一个虚拟线程。这种 JVM 内线程交换 比交换 platform/kernel 线程便宜得多 。 platform/kernel 承载这些不同虚拟线程的线程可以保持忙碌而不是等待每个块通过。

有关更多信息,请参阅 Oracle Project Loom 的 Ron Pressler 最近(2020 年底)的任何演讲,以及他在 2020-05 年发表的论文 State of Loom。这种快速交换阻塞虚拟线程的行为非常高效,以至于 CPU 可以一直保持忙碌。我们可以在 Activity Monitor 应用程序中确认此效果。这是 Activity 监控 运行 使用虚拟线程管理百万任务的屏幕截图。请注意,在所有百万个线程完成 12 秒的休眠后,CPU 内核几乎 100% 忙碌。

所以所有的工作都立即有效地完成了,因为所有百万个线程都同时 小睡了 12 秒,而 platform/kernel 个线程在分组中连续小睡五个。我们在上面的屏幕截图中看到,数百万个任务的工作是如何在几秒钟内同时完成的,而 platform/kernel 个线程执行相同数量的工作,但将其分散在几天内。

请注意,只有当您的任务经常被阻塞时,才会出现这种显着的性能提升。如果使用 CPU 绑定任务,例如视频编码,那么您应该使用 platform/kernel 线程而不是虚拟线程。大多数业务应用程序都会遇到很多阻塞,例如等待调用文件系统、数据库、其他外部服务或网络以访问远程服务。虚拟线程在那种经常阻塞的工作负载中大放异彩。