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

banq發表於2021-06-16

使用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)


 

相關文章