簡體   English   中英

究竟是什么讓 Java 虛擬線程更好

[英]What exactly makes Java Virtual Threads better

我很喜歡 Project Loom,但有一件事我不能完全理解。

大多數 Java 服務器使用具有一定線程限制(200、300 ..)的線程池,但是您不受操作系統的限制以產生更多,我已經閱讀了 Linux 的特殊配置,您可以達到巨大的數量。

操作系統線程更昂貴,啟動/停止速度更慢,必須處理上下文切換(由它們的數量放大),並且您依賴於可能拒絕為您提供更多線程的操作系統。

話雖如此,虛擬線程也消耗相似數量的 memory (或者至少這是我所理解的)。 使用 Loom,我們得到了尾調用優化,這應該會減少 memory 的使用。 同步和線程上下文復制也應該是一個類似大小的問題。

事實上,您能夠產生數百萬個虛擬線程

public static void main(String[] args) {
    for (int i = 0; i < 1_000_000; i++) {
        Thread.startVirtualThread(() -> {
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }
}

當我使用平台線程時,上面的代碼會在 25k 左右出現 OOM 異常。

我的問題是,究竟是什么讓這些線程如此輕巧,是什么阻止我們生成 100 萬個平台線程並與它們一起工作,是不是只有上下文切換才使常規線程如此“沉重”。

一個非常相似的問題

到目前為止我發現的東西:

  • 上下文切換是昂貴的 一般而言,即使在操作系統知道線程如何運行的理想情況下,它仍然必須給每個線程同等的執行機會,因為它們具有相同的優先級。 如果我們生成 10k 個 OS 線程,它將不得不在它們之間不斷切換,並且在某些情況下,僅此任務就可能占用高達 80% 的 CPU 時間,因此我們必須非常小心這些數字。 使用虛擬線程上下文切換由 JVM 完成,這使得它基本上是免費的
  • 便宜的啟動/停止 當我們中斷一個線程時,我們實際上是在告訴任務,“殺死你正在運行的操作系統線程”。 但是,例如,如果該線程位於線程池中,那么當我們詢問時,該線程可能會被當前任務釋放,然后交給另一個任務,而另一個任務可能會收到中斷信號。 這使得中斷過程相當復雜。 虛擬線程只是堆中的對象,我們可以讓 GC 在后台收集它們
  • 由於操作系統處理線程的方式,線程的硬上限(最多數萬)。 操作系統無法針對特定的應用程序和編程語言進行微調,因此它必須明智地為最壞的情況做好准備。 它必須分配更多實際用於滿足所有需求的 memory。 在執行所有這些操作時,它必須確保重要的操作系統進程仍在工作。 使用 VT,您僅受便宜的 memory 限制
  • 執行事務的線程的行為與執行視頻處理的線程非常不同,再次,操作系統必須為最壞的情況做准備,並以最好的方式適應這兩種情況,這意味着我們在大多數情況下獲得次優性能。 由於 VT 由 Java 本身產生和管理,因此可以完全控制它們以及不受操作系統約束的任務特定優化
  • 可調整大小的堆棧 操作系統為線程提供了一個大堆棧以適應所有用例,虛擬線程具有可調整大小的堆棧,位於堆空間中,動態調整大小以適應問題,使其更小
  • 更小的元數據大小 如上所述,平台線程使用 1MB,而虛擬線程需要 200-300 字節來存儲其元數據

協程(即虛擬線程)的一大優點是它們可以生成高水平的並發性,而沒有回調的缺點。

我先介紹一下利特爾定律:

concurrency = arrival_rate * latency

我們可以將其重寫為:

arrival_rate = concurrency/latency

在穩定的系統中,到達率等於吞吐量。

throughput = concurrency/latency

要提高吞吐量,您有 2 個選項:

  1. 減少延遲; 這通常非常困難,因為您對遠程調用或磁盤請求所花費的時間幾乎沒有影響。
  2. 增加並發

對於常規線程,由於上下文切換開銷,很難通過阻塞調用達到高並發性。 在某些情況下可以異步發出請求(例如 NIO + Epoll 或 Netty io_uring 綁定),但是您需要處理回調和回調地獄。

使用虛擬線程,可以異步發出請求並停放虛擬線程並調度另一個虛擬線程。 一旦收到響應,虛擬線程就會被重新調度,並且這是完全透明的。 編程 model 比使用經典線程和回調更直觀。

從根本上說,線程的任何實現,無論是輕量級還是重量級,都依賴於兩個結構

  • 任務
  • 調度器

線程有兩種任務類型

  • IO 綁定
  • CPU 綁定

並發與操作系統調度程序有關,並且在線程生命周期中具有非阻塞 IO 任務(與並行性不同)。 I/O 綁定程序與 CPU 綁定程序相反。 此類程序大部分時間都在等待輸入或 output 操作在 CPU 空閑時完成。 I/O 操作可以包括從主 memory 或網絡接口寫入或讀取的操作。

NIO 編程范式是我們談論並發性時首先想到的主題之一(從 2011 年 7 月開始,JDK 7 在 JDK 8 中部分和完全引入),因此多線程和管理線程的生命周期從線程池和適當的回調大致說我們可以實現這一點。

但另一方面,JVM 線程(基於守護進程和用戶)都包裹在操作系統線程上,操作系統線程是昂貴的資源,我們對它們的適用數量有限制。 這是虛擬線程或項目織機實踐。

在 OpenJDK 的最新原型中,一個名為 Fiber 的新 class 與線程 class 一起被引入庫。 由於計划中的 Fibers 庫與 Thread 相似,因此用戶實現也應該保持相似。 但是,有兩個主要區別:

  • Fiber 會將任何任務包裝在內部用戶模式延續中。 這將允許任務在 Java 運行時而不是 kernel 中暫停和恢復
  • 將使用可插入的用戶模式調度程序(例如 ForkJoinPool)

你可以在這里找到更多。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM