Java、Rust、Go、NodeJS、TypeScript併發程式設計比較

使用Java、Rust、Go、JavaScript (NodeJS)、TypeScript 等流行語言構建併發 Web 伺服器並對其進行基準測試(Deno) 和 Kotlin 來比較這些語言/平臺之間的併發性及其效能。

Rust 中的併發

高效和記憶體安全的併發是 Rust 的主要目標之一,這些不僅僅是簡單的詞,該語言為併發程式設計提供了強大的功能,當與同類最佳的記憶體安全模型相結合時,使其成為併發用例的絕佳選擇。

Rust 提供構建塊來建立和管理作業系統執行緒作為標準庫的一部分,它還提供使用通道的訊息傳遞併發(類似於 Go)和使用互斥體和智慧指標的共享狀態併發所需的實現。Rust 的型別系統和所有權模型有助於避免常見的併發問題,如資料競爭、鎖等。

最新版本的 Rust 提供了使用async/。await語法進行

非同步

程式設計所需的構建塊和語言功能。但請記住,使用非同步程式設計模型會增加整體複雜性,而且生態系統仍在不斷髮展。雖然 Rust 提供了所需的語言功能,但標準庫不提供任何所需的實現,因此您必須使用外部 crateFutures才能有效地使用非同步程式設計模型。

帶有 Tokio 的非同步多執行緒併發網路伺服器:這是另一個使用Tokio的非同步多執行緒網路伺服器版本,由Remco Bloemen貢獻。為簡潔起見,我省略了匯入語句。您可以在GitHub 上找到完整示例。

#[tokio::main()] // 預設情況下,Tokio 使用一個執行緒池大小適合 CPU 數量非同步 fn main() {    讓 listener = TcpListener::bind( “127。0。0。1:8080” )。await。unwrap();  // 繫結監聽器    let mut count = 0; // 用於引入延遲的計數    // 監聽傳入的連線。    環形 {        計數 = 計數 + 1;        讓 (socket, _) = listener。accept()。await。unwrap();        // 在新的 tokio 執行緒中非同步        生成每個連線tokio::spawn(async move { handle_connection(socket, Box:: new (count))。await });    }}非同步 fn handle_connection(mut 流:TcpStream,計數:Box){    // 從流中讀取前 1024 個位元組的資料    讓 mut 緩衝區 = [0; [1024];    stream。read(&mut buffer)。await。unwrap();    // 每 10 個請求新增 2 秒延遲if (*count % 10) == 0 {            println!( “新增延遲。計數:{}” , count);        sleep(Duration::from_secs(2))。await;    }    讓標題 = “    HTTP/1。0 200 正常    連線:保持連線    內容長度:174    內容型別:文字/html;字符集=utf-8        ”;    讓內容 = read_to_string( “hello。html” )。await。unwrap();    let response = format!( “{}\r\n\r\n{}” , 標題, 內容);    stream。write_all(response。as_bytes())。await。unwrap(); // 寫響應}

Threadpool除了非同步呼叫之外,也有來自執行緒池的相同瓶頸,因此我們將執行緒池設定為 100 以匹配最大併發請求。

讓我們使用 ApacheBench 執行一個基準測試。我們將發出 10000 個請求和 100 個併發請求。

ab -c 100 -n 10000 http://127。0。0。1:8080/這是 ApacheBench,版本 2。3 <$Revision: 1879490 $>。。。檔案路徑:/檔案長度:176 位元組併發級別:100拍攝時間為測試:20。569秒完成請求:10000失敗的請求:0總傳輸量:3030000 位元組傳輸的 HTML:1760000 位元組每秒請求數:486。17 [#/sec](平均值)每個請求的時間:205。688 [ms](平均值)每個請求的時間:2。057 [ms](平均,跨所有併發請求)傳輸速率:143。86 [Kbytes/sec] 接收連線時間(毫秒)              最小平均值[+/-sd] 中值最大值連線:0 1 2。4 0 22處理:0 202 600。3 1 2013等待:0 202 600。3 1 2012總計:0 203 600。3 2 2029特定時間內服務的請求百分比(毫秒)  50%      2  66%      3  75%      5  80%      7  90%   2000  95%   2003  98%   2006  99%   2008 100% 2029(最長請求)

Java 中的併發

這個例子更接近Rust語言的非同步例子,為了簡潔我省略了 import 語句。您可以在GitHub 上找到完整示例。請注意,我們在java。nio。channels。AsynchronousServerSocketChannel這裡使用並且沒有外部依賴項。

公共 類JavaAsyncHTTPServer {    public static void main(String[] args) 丟擲異常 {        new JavaAsyncHTTPServer()。start();        Thread。currentThread()。join(); // 永遠等待    }    private void start() throws IOException {        // 我們不應該在這裡使用 try with resource 因為它會殺死流var server = AsynchronousServerSocketChannel。open();                server。bind( new InetSocketAddress( “127。0。0。1” , 8080), 100); // 繫結監聽器        server。setOption(StandardSocketOptions。SO_REUSEADDR, true );        System。out。println( “伺服器正在監聽8080埠” );        最終 int [] 計數 = {0};// 用於引入延遲的計數        // 監聽所有傳入請求        server。accept( null , new CompletionHandler<>() {            @覆蓋            公共 無效完成(最終的AsynchronousSocketChannel結果,最終的物件附件){                如果(server。isOpen()){                    server。accept( null , this );                }                計數[0]++;                handleAcceptConnection(result, count[0]);            }            @覆蓋            public void failed( final Throwable exc, final Object attach ) {                if (server。isOpen()) {                    server。accept( null , this );                    System。out。println( “連線處理程式錯誤:” + exc);                }            }        });    }    private void handleAcceptConnection( final AsynchronousSocketChannel ch, final int count) {        var file = new File( “hello。html” );        try ( var fileIn = new FileInputStream(file)) {            // 每 10 個請求新增 2 秒延遲if (count % 10 == 0) {                            System。out。println( “新增延遲。計數:” + count);                執行緒睡眠(2000);            }            if (ch != null && ch。isOpen()) {                // 從流中讀取前 1024 個位元組的資料final ByteBuffer buffer = ByteBuffer。allocate(1024);                // 完全讀取請求以避免連線重置錯誤                                ch。read(buffer)。get();                // 讀取 HTML 檔案var fileLength = ( int ) file。length();                var fileData =新位元組[檔案長度];                                fileIn。read (fileData);                // 傳送 HTTP 標頭var message = ( “HTTP/1。1 200 OK\n” +                        “Connection: keep-alive\n” +                        “Content-length: ” + fileLength + “\n” +                        “Content-Type: text/ html; charset=utf-8\r\n\r\n” +                        new String(fileData, StandardCharsets。UTF_8)                                )。getBytes();                // 寫入輸出流                ch。write(ByteBuffer。wrap(message))。get();                緩衝區清除();                ch。close();            }        } catch (IOException | InterruptedException | ExecutionException e) {            System。out。println( “連線處理程式錯誤:” + e);        }    }}

我們將非同步偵聽器繫結到埠 8080 並偵聽所有傳入請求。每個請求都在由 AsynchronousServerSocketChannel提供的新任務中處理。我們在這裡沒有使用任何執行緒池,所有傳入的請求都是非同步處理的,因此我們沒有最大連線數的瓶頸。

讓我們使用 ApacheBench 執行一個基準測試。我們將發出 10000 個請求和 100 個併發請求。

ab -c 100 -n 10000 http://127。0。0。1:8080/這是 ApacheBench,版本 2。3 <$Revision: 1879490 $>。。。檔案路徑:/檔案長度:176 位元組併發級別:100拍攝時間為測試:20。243秒完成請求:10000失敗的請求:0總傳輸量:2770000 位元組傳輸的 HTML:1760000 位元組每秒請求數:494。00 [#/sec](平均值)每個請求的時間:202。431 [ms](平均值)每個請求的時間:2。024 [ms](平均,跨所有併發請求)傳輸速率:133。63 [Kbytes/sec] 接收連線時間(毫秒)              最小平均值[+/-sd] 中值最大值連線:0 0 0。6 0 5處理:0 201 600。0 0 2026等待:0 201 600。0 0 2026總計:0 202 600。0 0 2026特定時間內服務的請求百分比(毫秒)  50%      0  66%      1  75%      3  80%      4  90%   2000  95%   2001  98%   2002  99%   2003 100% 2026(最長請求)

Go 中的併發

不要透過共享記憶體進行通訊;相反,透過通訊共享記憶體。Go 支援併發作為一等公民,其goroutines。 Go 將協程的概念提升到一個全新的水平,使其更簡單,並且成為在 Go 中執行幾乎任何事情的首選方式。語義和語法非常簡單,即使是 Go 新手也能從一開始就goroutines輕鬆上手。所有這一切都沒有犧牲效能。

為簡潔起見,我省略了匯入語句。您可以在GitHub 上找到完整示例。在這種情況下,我們也沒有使用任何外部依賴項,並且http是 Go 標準庫的一部分。

func main() {     var count = 0     // 設定路由器    http。HandleFunc( “/” , func(w http。ResponseWriter, r *http。Request) {        延遲 r。Body。Close()        計數++        控制代碼連線(w,計數)    })    // 設定監聽埠    err := http。ListenAndServe( “:8080” , nil)     if err != nil {        log。Fatal( “ListenAndServe:” , 錯誤)    }}func handleConnection(w http。ResponseWriter, count int ) {     // 每 10 個請求新增 2 秒延遲if (count % 10) == 0 {            println( “新增延遲。計數:” , count)        time。Sleep(2 * time。Second)    }    html, _ := ioutil。ReadFile( “hello。html” ) // 讀取 html 檔案    w。Header()。Add( “Connection” , “keep-alive” )    w。WriteHeader(200)           // 200 OK     fmt。Fprintf(w, string(html)) // 向客戶端傳送資料}

如您所見,我們建立了一個繫結到埠 8080 的 HTTP 伺服器並偵聽所有傳入請求。我們分配一個回撥函式來處理內部呼叫handleConnection方法的每個請求。

讓我們使用 ApacheBench 執行一個基準測試。我們將發出 10000 個請求和 100 個併發請求。

ab -c 100 -n 10000 http://127。0。0。1:8080/這是 ApacheBench,版本 2。3 <$Revision: 1879490 $>。。。檔案路徑:/檔案長度:174 位元組併發級別:100拍攝時間為測試:20。232秒完成請求:10000失敗的請求:0總傳輸量:2910000 位元組傳輸的 HTML:1740000 位元組每秒請求數:494。27 [#/sec](平均值)每個請求的時間:202。319 [ms](平均值)每個請求的時間:2。023 [ms](平均,跨所有併發請求)傳輸速率:140。46 [Kbytes/sec] 接收連線時間(毫秒)              最小平均值[+/-sd] 中值最大值連線:0 1 0。9 0 6處理:0 201 600。0 1 2013等待:0 201 600。0 0 2013總計:0 202 600。0 1 2018警告:中位數和平均為初始連線時是不正常的偏差範圍內        這些結果可能並不那麼可靠。特定時間內服務的請求百分比(毫秒)  50%      1  66%      1  75%      2  80%      3  90%   2000  95%   2001  98%   2002  99%   2003 100% 2018(最長請求)

JavaScript 和 NodeJS 中的併發

JavaScript 是單執行緒的,因此實現多執行緒的唯一方法是啟動 JS 引擎的多個例項。但是,您如何在這些例項之間進行通訊?這就是Web Workers 的用武之地。

Web Workers 使在與 Web 應用程式的主執行執行緒分離的後臺執行緒中執行指令碼操作成為可能

在 Web Worker 的幫助下,可以將繁重的計算解除安裝到單獨的執行緒,從而釋放主執行緒。這些工作執行緒和主執行緒使用事件進行通訊,一個工作執行緒可以產生其他工作執行緒。

現在,當談到 NodeJS 時,幾乎沒有方法可以產生額外的執行緒和程序。有經典child_process模組,更現代的worker_threads模組,與 Web Worker 非常相似,以及cluster用於建立 NodeJS 例項叢集的模組。

無論是 web worker 還是 worker 執行緒,它們都不像其他語言中的多執行緒實現那樣靈活或簡單,並且有很多限制,因此它們大多隻在有 CPU 密集型任務或後臺任務需要執行以供其他用途時使用使用非同步處理的併發情況就足夠了。

JavaScript 不提供對 OS 執行緒或綠色執行緒的訪問,同樣適用於 NodeJS 但是工作執行緒和叢集很接近,因此高階多執行緒是不可行的。訊息傳遞併發是可能的,由 JS 事件迴圈本身使用,可用於 JS 中的 Worker 和標準併發模型。在標準併發模型和使用陣列緩衝區的工作執行緒中,共享狀態併發是可能的。

我們使用cluster模組來分叉主執行緒和工作執行緒,每個 CPU 執行緒一個工作執行緒。我們仍然在http這裡使用模組和回撥。您可以在GitHub 上找到完整示例。在這種情況下,我們也沒有使用任何外部依賴。

const http = require(“http”);const fs = require(“fs”)。promises;const cluster = require(“cluster”);const numCPUs = require(“os”)。cpus()。length;let count = 0;// set routerconst server = http。createServer((req, res) => {  count++;  requestListener(req, res, count);});const host = “localhost”;const port = 8080;if (cluster。isMaster) {  console。log(`Master ${process。pid} is running`);  // Fork workers。  for (let i = 0; i < numCPUs; i++) {    cluster。fork();  }  cluster。on(“exit”, (worker, code, signal) => {    console。log(`worker ${worker。process。pid} died`);  });} else {  // set listen port, TCP connection is shared by all workers  server。listen(port, host, () => {    console。log(`Worker ${process。pid}: Server is running on http://${host}:${port}`);  });}const requestListener = async function (req, res, count) {  // add 2 second delay to every 10th request  if (count % 10 === 0) {    console。log(“Adding delay。 Count: ”, count);    await sleep(2000);  }  const contents = await fs。readFile(__dirname + “/hello。html”); // read html file  res。setHeader(“Connection”, “keep-alive”);  res。writeHead(200); // 200 OK  res。end(contents); // send data to client side};// sleep function since NodeJS doesn‘t provide onefunction sleep(ms) {  return new Promise((resolve) => {    setTimeout(resolve, ms);  });}

叢集模組分為 master 和 worker。我們分配一個回撥函式來處理內部呼叫該requestListener方法的每個請求。

讓我們使用 ApacheBench 執行一個基準測試。我們將發出 10000 個請求和 100 個併發請求。

ab -c 100 -n 10000 http://127。0。0。1:8080/This is ApacheBench, Version 2。3 <$Revision: 1879490 $>。。。Server Software:Server Hostname:        127。0。0。1Server Port:            8080Document Path:          /Document Length:        174 bytesConcurrency Level:      100Time taken for tests:   21。075 secondsComplete requests:      10000Failed requests:        0Total transferred:      2540000 bytesHTML transferred:       1740000 bytesRequests per second:    474。50 [#/sec] (mean)Time per request:       210。747 [ms] (mean)Time per request:       2。107 [ms] (mean, across all concurrent requests)Transfer rate:          117。70 [Kbytes/sec] receivedConnection Times (ms)              min  mean[+/-sd] median   maxConnect:        0    0   0。8      0      11Processing:     0  206 600。1      4    2047Waiting:        0  205 600。1      3    2045Total:          1  206 600。1      4    2047Percentage of the requests served within a certain time (ms)  50%      4  66%      8  75%     11  80%     14  90%     88  95%   2005  98%   2012  99%   2016 100%   2047 (longest request)

Deno 中的併發

TypeScript 中的併發性與 JavaScript 中的完全相同,因為 TypeScript 是 JavaScript 的嚴格超集。

因此,如果您將 TypeScript 與 NodeJS 一起使用,它與在 NodeJS 上使用 JavaScript 完全相同,因為 NodeJS 不會在本地執行 TypeScript,我們必須將其轉換為 JavaScript,因此讓我們專注於 Deno 上的 TypeScript,因為我們已經涵蓋了 NodeJS。

與 NodeJS 不同,Deno 可以在本地執行 TypeScript,它會在幕後轉換為 JS。正如我們在 NodeJS 中看到的,Deno 還專注於非阻塞 IO,旨在改進/修復 NodeJS 中的問題。這意味著你也可以在 Deno 上使用 NodeJS 和 JavaScript 完成所有可以做的事情,有時使用更好的 API 和更少的程式碼。就像在 JS 中一樣,您依靠事件迴圈、回撥、承諾和Async/Await來實現 TypeScript 中的併發。

Deno API 預設是非同步的,並且推薦經常使用 async/await 。

Deno 中的預設併發是使用回撥、Promise 或 async/await 的非同步程式設計模型。

就像在 JavaScript 中一樣,也可以在 Deno 上使用 TypeScript 進行某種程度的多執行緒併發和並行化,並且由於 Deno 是基於 Rust 構建的,因此未來併發效能可能會比NodeJS 上的更好。

這個例子更接近Rust 非同步例子。您可以在此處在GitHub 上找到完整示例。在這種情況下,我們僅使用標準 Deno 模組。

import { serve, ServerRequest } from “https://deno。land/std/http/server。ts”;let count = 0;// set listen portconst server = serve({ hostname: “0。0。0。0”, port: 8080 });console。log(`HTTP webserver running at:  http://localhost:8080/`);// listen to all incoming requestsfor await (const request of server) handleRequest(request);async function handleRequest(request: ServerRequest) {  count++;  // add 2 second delay to every 10th request  if (count % 10 === 0) {    console。log(“Adding delay。 Count: ”, count);    await sleep(2000);  }  // read html file  const body = await Deno。readTextFile(“。/hello。html”);  const res = {    status: 200,    body,    headers: new Headers(),  };  res。headers。set(“Connection”, “keep-alive”);  request。respond(res); // send data to client side}// sleep function since NodeJS doesn’t provide onefunction sleep(ms: number) {  return new Promise((resolve) => {    setTimeout(resolve, ms);  });}

我們建立了一個 HTTP 伺服器並將其繫結到埠 8080 並在 for await 迴圈中偵聽所有傳入請求。每個請求都在內部使用async/await函式處理。

讓我們使用 ApacheBench 執行一個基準測試。我們將發出 10000 個請求和 100 個併發請求。

ab -k -c 100 -n 10000 http://127。0。0。1:8080/This is ApacheBench, Version 2。3 <$Revision: 1879490 $>。。。Server Software:Server Hostname:        127。0。0。1Server Port:            8080Document Path:          /Document Length:        174 bytesConcurrency Level:      100Time taken for tests:   21。160 secondsComplete requests:      10000Failed requests:        0Keep-Alive requests:    10000Total transferred:      2380000 bytesHTML transferred:       1740000 bytesRequests per second:    472。59 [#/sec] (mean)Time per request:       211。600 [ms] (mean)每個請求的時間:2。116 [ms](平均,跨所有併發請求)傳輸速率:109。84 [Kbytes/sec] 接收連線時間(毫秒)              最小平均值[+/-sd] 中值最大值連線:0 0 0。7 0 11處理:0 207 600。7 5 2250等待:0 207 600。7 5 2250總計:0 207 600。7 5 2254特定時間內服務的請求百分比(毫秒)  50%      5  66%      8  75%     11  80%     13  90%   2001  95%   2006  98%   2012  99%   2017 100% 2254(最長請求)