Java、Rust、Go、NodeJS、TypeScript併發程式設計比較 - foojay
使用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 uses a threadpool sized for number of cpus by default async fn main() { let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap(); // bind listener let mut count = 0; // count used to introduce delays // Listen for an incoming connection. loop { count = count + 1; let (socket, _) = listener.accept().await.unwrap(); // spawning each connection in a new tokio thread asynchronously tokio::spawn(async move { handle_connection(socket, Box::new(count)).await }); } } async fn handle_connection(mut stream: TcpStream, count: Box<i64>) { // Read the first 1024 bytes of data from the stream let mut buffer = [0; 1024]; stream.read(&mut buffer).await.unwrap(); // add 2 second delay to every 10th request if (*count % 10) == 0 { println!("Adding delay. Count: {}", count); sleep(Duration::from_secs(2)).await; } let header = " HTTP/1.0 200 OK Connection: keep-alive Content-Length: 174 Content-Type: text/html; charset=utf-8 "; let contents = read_to_string("hello.html").await.unwrap(); let response = format!("{}\r\n\r\n{}", header, contents); stream.write_all(response.as_bytes()).await.unwrap(); // write response } |
Threadpool除了非同步呼叫之外,也有來自執行緒池的相同瓶頸,因此我們將執行緒池設定為 100 以匹配最大併發請求。
讓我們使用 ApacheBench 執行一個基準測試。我們將發出 10000 個請求和 100 個併發請求。
ab -c 100 -n 10000 http://127.0.0.1:8080/ This is ApacheBench, Version 2.3 <$Revision: 1879490 $> ... Document Path: / Document Length: 176 bytes Concurrency Level: 100 Time taken for tests: 20.569 seconds Complete requests: 10000 Failed requests: 0 Total transferred: 3030000 bytes HTML transferred: 1760000 bytes Requests per second: 486.17 [#/sec] (mean) Time per request: 205.688 [ms] (mean) Time per request: 2.057 [ms] (mean, across all concurrent requests) Transfer rate: 143.86 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 1 2.4 0 22 Processing: 0 202 600.3 1 2013 Waiting: 0 202 600.3 1 2012 Total: 0 203 600.3 2 2029 Percentage of the requests served within a certain time (ms) 50% 2 66% 3 75% 5 80% 7 90% 2000 95% 2003 98% 2006 99% 2008 100% 2029 (longest request) |
Java 中的併發
這個例子更接近Rust語言的非同步例子,為了簡潔我省略了 import 語句。您可以在GitHub 上找到完整示例。請注意,我們在java.nio.channels.AsynchronousServerSocketChannel這裡使用並且沒有外部依賴項。
public class JavaAsyncHTTPServer { public static void main(String[] args) throws Exception { new JavaAsyncHTTPServer().start(); Thread.currentThread().join(); // Wait forever } private void start() throws IOException { // we shouldn't use try with resource here as it will kill the stream var server = AsynchronousServerSocketChannel.open(); server.bind(new InetSocketAddress("127.0.0.1", 8080), 100); // bind listener server.setOption(StandardSocketOptions.SO_REUSEADDR, true); System.out.println("Server is listening on port 8080"); final int[] count = {0}; // count used to introduce delays // listen to all incoming requests server.accept(null, new CompletionHandler<>() { @Override public void completed(final AsynchronousSocketChannel result, final Object attachment) { if (server.isOpen()) { server.accept(null, this); } count[0]++; handleAcceptConnection(result, count[0]); } @Override public void failed(final Throwable exc, final Object attachment) { if (server.isOpen()) { server.accept(null, this); System.out.println("Connection handler error: " + exc); } } }); } private void handleAcceptConnection(final AsynchronousSocketChannel ch, final int count) { var file = new File("hello.html"); try (var fileIn = new FileInputStream(file)) { // add 2 second delay to every 10th request if (count % 10 == 0) { System.out.println("Adding delay. Count: " + count); Thread.sleep(2000); } if (ch != null && ch.isOpen()) { // Read the first 1024 bytes of data from the stream final ByteBuffer buffer = ByteBuffer.allocate(1024); // read the request fully to avoid connection reset errors ch.read(buffer).get(); // read the HTML file var fileLength = (int) file.length(); var fileData = new byte[fileLength]; fileIn.read(fileData); // send HTTP Headers 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(); // write the to output stream ch.write(ByteBuffer.wrap(message)).get(); buffer.clear(); ch.close(); } } catch (IOException | InterruptedException | ExecutionException e) { System.out.println("Connection handler error: " + e); } } } |
我們將非同步偵聽器繫結到埠 8080 並偵聽所有傳入請求。每個請求都在由 AsynchronousServerSocketChannel提供的新任務中處理。我們在這裡沒有使用任何執行緒池,所有傳入的請求都是非同步處理的,因此我們沒有最大連線數的瓶頸。
讓我們使用 ApacheBench 執行一個基準測試。我們將發出 10000 個請求和 100 個併發請求。
ab -c 100 -n 10000 http://127.0.0.1:8080/ This is ApacheBench, Version 2.3 <$Revision: 1879490 $> ... Document Path: / Document Length: 176 bytes Concurrency Level: 100 Time taken for tests: 20.243 seconds Complete requests: 10000 Failed requests: 0 Total transferred: 2770000 bytes HTML transferred: 1760000 bytes Requests per second: 494.00 [#/sec] (mean) Time per request: 202.431 [ms] (mean) Time per request: 2.024 [ms] (mean, across all concurrent requests) Transfer rate: 133.63 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.6 0 5 Processing: 0 201 600.0 0 2026 Waiting: 0 201 600.0 0 2026 Total: 0 202 600.0 0 2026 Percentage of the requests served within a certain time (ms) 50% 0 66% 1 75% 3 80% 4 90% 2000 95% 2001 98% 2002 99% 2003 100% 2026 (longest request) |
Go 中的併發
不要透過共享記憶體進行通訊;相反,透過通訊共享記憶體。Go 支援併發作為一等公民,其goroutines. Go 將協程的概念提升到一個全新的水平,使其更簡單,並且成為在 Go 中執行幾乎任何事情的首選方式。語義和語法非常簡單,即使是 Go 新手也能從一開始就goroutines輕鬆上手。所有這一切都沒有犧牲效能。
為簡潔起見,我省略了匯入語句。您可以在GitHub 上找到完整示例。在這種情況下,我們也沒有使用任何外部依賴項,並且http是 Go 標準庫的一部分。
func main() { var count = 0 // set router http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() count++ handleConnection(w, count) }) // set listen port err := http.ListenAndServe(":8080", nil) if err != nil { log.Fatal("ListenAndServe: ", err) } } func handleConnection(w http.ResponseWriter, count int) { // add 2 second delay to every 10th request if (count % 10) == 0 { println("Adding delay. Count: ", count) time.Sleep(2 * time.Second) } html, _ := ioutil.ReadFile("hello.html") // read html file w.Header().Add("Connection", "keep-alive") w.WriteHeader(200) // 200 OK fmt.Fprintf(w, string(html)) // send data to client side } |
如您所見,我們建立了一個繫結到埠 8080 的 HTTP 伺服器並偵聽所有傳入請求。我們分配一個回撥函式來處理內部呼叫handleConnection方法的每個請求。
讓我們使用 ApacheBench 執行一個基準測試。我們將發出 10000 個請求和 100 個併發請求。
ab -c 100 -n 10000 http://127.0.0.1:8080/ This is ApacheBench, Version 2.3 <$Revision: 1879490 $> ... Document Path: / Document Length: 174 bytes Concurrency Level: 100 Time taken for tests: 20.232 seconds Complete requests: 10000 Failed requests: 0 Total transferred: 2910000 bytes HTML transferred: 1740000 bytes Requests per second: 494.27 [#/sec] (mean) Time per request: 202.319 [ms] (mean) Time per request: 2.023 [ms] (mean, across all concurrent requests) Transfer rate: 140.46 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 1 0.9 0 6 Processing: 0 201 600.0 1 2013 Waiting: 0 201 600.0 0 2013 Total: 0 202 600.0 1 2018 WARNING: The median and mean for the initial connection time are not within a normal deviation These results are probably not that reliable. Percentage of the requests served within a certain time (ms) 50% 1 66% 1 75% 2 80% 3 90% 2000 95% 2001 98% 2002 99% 2003 100% 2018 (longest request) |
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 router const 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 one function 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.1 Server Port: 8080 Document Path: / Document Length: 174 bytes Concurrency Level: 100 Time taken for tests: 21.075 seconds Complete requests: 10000 Failed requests: 0 Total transferred: 2540000 bytes HTML transferred: 1740000 bytes Requests 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] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.8 0 11 Processing: 0 206 600.1 4 2047 Waiting: 0 205 600.1 3 2045 Total: 1 206 600.1 4 2047 Percentage 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 port const server = serve({ hostname: "0.0.0.0", port: 8080 }); console.log(`HTTP webserver running at: http://localhost:8080/`); // listen to all incoming requests for 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 one function 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.1 Server Port: 8080 Document Path: / Document Length: 174 bytes Concurrency Level: 100 Time taken for tests: 21.160 seconds Complete requests: 10000 Failed requests: 0 Keep-Alive requests: 10000 Total transferred: 2380000 bytes HTML transferred: 1740000 bytes Requests per second: 472.59 [#/sec] (mean) Time per request: 211.600 [ms] (mean) Time per request: 2.116 [ms] (mean, across all concurrent requests) Transfer rate: 109.84 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.7 0 11 Processing: 0 207 600.7 5 2250 Waiting: 0 207 600.7 5 2250 Total: 0 207 600.7 5 2254 Percentage of the requests served within a certain time (ms) 50% 5 66% 8 75% 11 80% 13 90% 2001 95% 2006 98% 2012 99% 2017 100% 2254 (longest request) |
相關文章
- Java JIT與AOT效能比較 - foojayJava
- Java,Go和Rust之間的比較 - DexterJavaGoRust
- Go 併發程式設計Go程式設計
- 學習Rust 併發程式設計Rust程式設計
- Rust, Go與Hasekll比較 - RedditRustGo
- Go 併發程式設計 - 併發安全(二)Go程式設計
- 併發程式設計:DEMO:比較Stream和forkjoin框架的效率程式設計框架
- Go語言併發程式設計Go程式設計
- go併發程式設計筆記Go程式設計筆記
- Go 併發程式設計之 MutexGo程式設計Mutex
- 八. Go併發程式設計--errGroupGo程式設計
- Go併發程式設計--Mutex/RWMutexGo程式設計Mutex
- Go併發程式設計基礎Go程式設計
- java 併發程式設計Java程式設計
- Java併發程式設計Java程式設計
- 三種Javascript(NodeJS)非同步程式設計程式碼模式比較JavaScriptNodeJS非同步程式設計模式
- 【Java併發程式設計】併發程式設計大合集-值得收藏Java程式設計
- Java併發程式設計 - 第十一章 Java併發程式設計實踐Java程式設計
- 十二. Go併發程式設計--sync/errGroupGo程式設計
- 十.Go併發程式設計--channel使用Go程式設計
- 【Go進階—併發程式設計】MutexGo程式設計Mutex
- 六. Go併發程式設計--WaitGroupGo程式設計AI
- 併發模型比較模型
- java-併發程式設計Java程式設計
- Java併發程式設計-CASJava程式設計
- Java併發程式設計:synchronizedJava程式設計synchronized
- Java 併發程式設計解析Java程式設計
- Java併發程式設計:LockJava程式設計
- Java併發程式設計---java規範與模式下的併發程式設計1.1Java程式設計模式
- Java併發程式設計-鎖及併發容器Java程式設計
- Java併發系列—併發程式設計挑戰Java程式設計
- java併發程式設計系列:java併發程式設計背景知識Java程式設計
- Go併發程式設計之美-CAS操作Go程式設計
- Go併發程式設計--正確使用goroutineGo程式設計
- Go併發程式設計之美-互斥鎖Go程式設計
- Go 併發程式設計 - Goroutine 基礎 (一)Go程式設計
- 七. Go併發程式設計--sync.OnceGo程式設計
- 十一. Go併發程式設計--singleflightGo程式設計