Loom
Loom
内核线程始终是一种稀缺资源。如果你真的把它归结为,这里真正的基础资源是硬件提供的物理线程,根据定义,物理线程是有限的。即使在抽象塔上,内核线程在操作系统本身和 JVM 中都是相对重要的。一般来说,一个进程很难拥有超过几千个线程,即使仔细调整也是如此,而拥有更少的线程则是更理想的。Loom 所做的是和 Cats Effect 这样的框架玩同样的把戏,也就是说,它在底层内核线程(它称之为 “载体线程”)之上创建一个抽象。这个抽象是非常轻量级的,而且严格来说(有点……)是非阻塞的,这使得在一个进程中拥有数百万个线程而不产生问题成为可能。也许令人困惑的是,Loom 将这个抽象定义为 Thread 本身,并将其直接集成到 JVM 中,这意味着任何在 JVM 上编写的代码都能够利用它(而不是像 Cats Effect 这样的框架,在那里你需要明确选择加入 IO 或 Future 等东西)。
因此,这里发生的事情是,Thread 被重新定义为一个更轻量级的抽象,位于底层载体线程之上,而底层载体线程与以往一样稀缺且重量级。这样做的好处是,你需要非常小心地处理那些硬性阻断底层载体线程的事情。Loom 试图通过与 JVM 和 Java 标准库的紧密结合来解决这个问题,这样一来,通常会阻断载体线程的机制反而会取消虚拟线程的时间安排,允许其他线程访问。更简单地说,它将 Unsafe.park 转换为一个回调,在运行时恢复线程的延续性。
这是一个聪明的技巧,特别是集成到 JVM 中,但它并不完美。正如你所指出的,本地代码中的任何阻塞都完全超出了 Loom 所能保护的范围,而且这种阻塞比你想象的要普遍得多。例如,Netty 在本地代码中非常积极地进行阻塞,因为它实现了自己的操作系统特定的异步 IO 层接口(如 epoll 和 io_uring)。即使没有第三方框架,本机阻塞的例子也比比皆是。new URL("https://www.google.com").hashCode()
就是一个例子,因为它委托给了本机操作系统的 DNS 客户端,而后者在所有主要操作系统上都是阻塞的。另一个例子是文件 IO,它在 NTFS 上是无阻塞的,在支持 io_uring 的 Linux 版本上也可以是无阻塞的,但在 APFS 和 HFS+上根本上是阻塞的。
换句话说,Loom 是一个典型的泄漏性抽象:它承诺了一些它无法实现的东西,并在这样做时邀请你编写代码,而这些假设在许多常见情况下是不成立的。这就是它与 Cats Effect 或 Vert.x 等框架的真正不同之处,后者非常直接地指出阻塞是不好的,并促使你(用户)努力声明你的阻塞,以便可以用不太危险的方式管理它(特别是通过分流策略,如 OP 中描述的)。