Thread-Per-Message

January 10, 2022

你寫了一個簡單的下載網頁程式:

import java.io.*;
import java.net.URI;
import java.net.http.*;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.file.*;
import static java.nio.file.StandardCopyOption.*;

public class Main {
    static InputStream openStream(String uri) throws Exception {
        return HttpClient
                 .newHttpClient()
                 .send(
                     HttpRequest.newBuilder(URI.create(uri)).build(), 
                     BodyHandlers.ofInputStream()
                 )
                 .body();
    }   
    
    static void download(String uri, String fileName) throws Exception {
        Files.copy(
           openStream(uri), Paths.get(fileName), REPLACE_EXISTING);
    }
    
    public static void main(String[] args) throws Exception {
        String[] uris = {
            "https://openhome.cc/zh-tw/algorithm/",
            "https://openhome.cc/zh-tw/computation/",
            "https://openhome.cc/zh-tw/toy-lang/",
            "https://openhome.cc/zh-tw/pattern/"
        };
        for(var uri : uris) {
            download(
                uri, 
                uri.replace("https://openhome.cc/zh-tw/", "")
                   .replace("/", "")
                   .concat(".html")
            );
        }
    }
}

一個請求一個執行緒

在單執行緒程式下,需要等上個請求完成,才能執行下個請求,你想了一下,下載後是直接存檔,不用取得 download 方法的傳回值,由於網路連線會有等待回應等空檔,若這時能再開啟下個請求,應該可以加快程式的執行。

for(var uri : uris) {
    new Thread(() -> {
        try {
            download(
                uri, 
                uri.replace("https://openhome.cc/zh-tw/", "")
                   .replace("/", "")
                   .concat(".html")
            );
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }).start();
}

一個請求一個執行緒,這就是 Thread-Per-Message,在執行緒的入門文件,應該都會有類似的範例吧!或者是寫桌面圖形介面時,為了避免視窗在操作某些輸入輸出時凍結,那些輸入輸出請求,通常都會另建一個執行緒來處理。

簡單到我都在想,這個名詞就不用介紹算了…XD

封裝執行緒的取得

不過呢!建立執行緒是要成本的,視窗程式之類的,偶而建立一下執行緒來完成輸入輸出,或者一些會阻斷的操作,是沒有關係,畢竟使用者的手速應該不致於快到,讓執行緒的建立成本產生負擔。

不過,若是一些自動化請求,或者是伺服器,每個請求就建立一個執行緒,那就要考量建立執行緒的成本了!比較好的方式是,封裝執行緒的取得方式,

class ThreadService {
    static void submit(Runnable runnable) {
        new Thread(runnable).start();
    }
}

很簡單的封裝概念!如此一來,客戶端就可以使用 ThreadService.submit

for(var uri : uris) {
    ThreadService.submit(() -> {
        try {
            download(
                uri, 
                uri.replace("https://openhome.cc/zh-tw/", "")
                   .replace("/", "")
                   .concat(".html")
            );
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    });
}

或許將來,你會進一步考量到重用執行緒之類的問題,那就修改 ThreadService.submit 就好,或者你會更進一步地,考量到各種不同的執行緒服務需求,像是執行緒排程等,你可能重構 ThreadService 之類…或者其實你早就知道,要是使用 Java 的話,標準 API 就提供了 Executor 這類框架。

Thread-Per-Message 就行為上應該解釋為,為每個請求「安排」一個執行緒,然而應該進一步考量到,對客戶端隱藏安排的細節,因為為每個請求各自安排執行緒,並不是只有新建執行緒這個選項!