CodeGym /課程 /C# SELF /非同步 vs. 多執行緒 ( async/await

非同步 vs. 多執行緒 ( async/awaitThread)

C# SELF
等級 59 , 課堂 1
開放

1. 介紹

在程式設計中,新手(甚至有經驗的開發者)常常把兩個看起來相似但實際上不同的概念弄混:多執行緒非同步。面試時常會問到這個問題,想看應徵者是不是真的理解差別,因為這會直接影響你如何寫出快速且回應靈敏的程式碼。

我們來釐清到底差在哪裡。

多執行緒:很多手一起工作

多執行緒 是指用多個執行緒來組織程式的工作。執行緒是一條執行線程,是處理器執行程式指令的獨立「通道」。一個程序(例如我們的 .NET 應用)可以啟動多個執行緒,讓不同任務同時進行。

生活中的例子: 你是專案經理,分派不同任務給不同同事,這些任務同時進行。有人寫報告、有人打電話給客戶、有人做簡報。

多執行緒的關鍵觀念: 任務是真正並行發生的(或者在單核心上透過快速切換上下文達到類似並行的效果)。

非同步:不空等、去做別的事

非同步 是把程式碼組織成在等待長時間操作(例如網路回應或檔案讀取)時能做別的事。非同步程式碼不一定會用到多個執行緒!它只是避免在等待某些操作時阻塞執行緒。

生活中的例子: 與其站在咖啡機前盯著咖啡滴,你把任務交給咖啡機,自己去回 e-mail、看新聞,等咖啡好了再去拿。

非同步的關鍵觀念: 等「長時間」任務時不要閒著,要去做有用的事情。

2. 差別在哪裡?

非同步與多執行緒常常一起使用,這也讓人更混亂。但事實上,這些技術回答的是不同的問題:

  • 多執行緒 用來真實地平行處理工作,利用可用的處理器/核心。
  • 非同步 用來智慧地組織等待資源(網路 I/O、磁碟 I/O 等),不去阻塞執行緒。

兩者可以結合使用,但它們不必然互相關聯。

簡短視覺化

多執行緒 (Threads) 非同步 (Async)
何時使用? 當任務佔用大量 CPU(CPU-bound:計算、渲染、處理陣列) 當任務在等待事件(I/O-bound:網路、磁碟、資料庫)
程式碼在做什麼? 讓處理器跑滿,啟動多個執行緒,真正並行 在等待資料時釋放執行緒 — 執行緒可以去做其它事
管理什麼? 同時執行的執行緒數量 誰現在忙/誰空閒,操作完成時要做什麼
典型範例 影片壓縮、影像渲染 檔案下載、向伺服器發送 HTTP 請求

範例 1:多執行緒 — 快速計算

假設我們有個重度任務:計算大數字的總和。


void ComputeSum(long start, long end)
{
    long sum = 0;
    for (long i = start; i <= end; i++)
    {
        sum += i;
    }
    Console.WriteLine($"總和從 {start} 到 {end} = {sum}");
}

// 同時啟動三個任務 — 每個計算自己的部分
Thread t1 = new Thread(() => ComputeSum(1, 1000_000_000));
Thread t2 = new Thread(() => ComputeSum(1000_000_001, 2000_000_000));
Thread t3 = new Thread(() => ComputeSum(2000_000_001, 3000_000_000));

t1.Start();
t2.Start();
t3.Start();

// 等待所有執行緒完成
t1.Join();
t2.Join();
t3.Join();

為什麼要用執行緒?
因為這是 CPU 密集的工作。如果你有多核心的機器,工作會被加速。

範例 2:非同步 — 等伺服器回應

網路很慢,在等待伺服器回應時,執行緒可以是「空閒」的。


// 非同步下載網頁,執行緒不會被阻塞
async Task DownloadPageAsync()
{
    using HttpClient client = new HttpClient();
    string html = await client.GetStringAsync("https://dotnet.microsoft.com/");
    Console.WriteLine(html.Length);
}

為什麼這裡要用非同步?
我們下達「開始下載」的指令,同時不去阻塞執行緒,等待資料到達的通知。

3. 非同步而沒有多執行緒:神話還是真實?

問題: 任何非同步程式碼都一定會啟動新執行緒嗎?
答案: 不!很多時候非同步根本不需要額外的執行緒。

例如當你呼叫 await file.ReadAsync(...) 時,.NET 會在作業系統層面啟動非同步操作,呼叫該方法的執行緒會立即變成「空閒」並回到執行緒池。操作完成時,執行緒池中的任一空閒執行緒會繼續執行你的工作。

  • 如果你改用同步版(file.Read(...))— 執行緒會傻等操作完成,什麼也不做。
  • 非同步程式碼的意思是:「處理器,等會兒我們在等的時候請去做別的事情!」

重要示意:


// 執行緒不會被阻塞,只是在等待操作完成
await Task.Delay(1000); // 只等一秒 — 不會佔用 CPU!

沒有非同步的多執行緒

有些情況下只有用執行緒平行化工作才有意義:重運算、大量資料處理等。這種情況下,用非同步並不能加速實際的計算,因為 CPU 本來就會被吃滿。

經典:處理大型檔案


// 這段程式碼真的會吃掉 CPU — 非同步無法幫忙
void CalculateHash(string file)
{
    byte[] data = File.ReadAllBytes(file); // 同步!
    // 計算雜湊...
}

想要加速就啟動多個執行緒,每個處理自己的檔案。

4. 在你的應用程式中長什麼樣子?

非同步操作 (await)

在我們的教學應用中,可以加上非同步載入資料。例如請求匯率或天氣資料 — 最好用非同步。


async Task GetWeatherAsync(string city)
{
    using HttpClient client = new HttpClient();
    string json = await client.GetStringAsync($"https://api.weather.com/{city}");
    // 收到回應後繼續處理
    Console.WriteLine($"天氣在 {city}: {json}");
}

底層發生了什麼?

呼叫 await 會把你的方法切成兩部分:

  • 發起非同步操作 — 作業系統執行緒「被釋放」,可以去做其他事。
  • 當資料到達時 — 方法在執行緒池中的某個空閒執行緒上繼續執行。

複雜計算用多執行緒

在我們的計算機範例(假設要處理大陣列)— 這時啟動單獨執行緒來做計算是有意義的。


// 把大任務分成小塊,每個執行緒計算自己的範圍
List<Thread> threads = new List<Thread>();
for (int i = 0; i < 4; i++)
{
    int rangeStart = i * 1000000;
    int rangeEnd = (i + 1) * 1000000 - 1;
    Thread t = new Thread(() => ComputeSum(rangeStart, rangeEnd));
    threads.Add(t);
    t.Start();
}
// 等待所有執行緒完成
foreach (Thread t in threads) t.Join();

5. 常見錯誤與非同步工作時的注意事項

錯誤 #1:把 async 當作「加速」手段。
很常見的誤解是認為 async 會讓程式跑得更快。事實不是這樣。非同步是為了回應性,而不是為了原地加速。

如果任務是 CPU-bound(處理器負載)— 非同步不會讓它更快。
如果任務是 I/O-bound(網路、磁碟)— 非同步很有用,因為執行緒不會閒置,可以去做其他工作。

錯誤 #2:透過 Wait()Result 阻塞執行緒。
在非同步程式碼中千萬不要呼叫 Wait() 或存取任務的 Result。這幾乎總是會造成執行緒被阻塞和 deadlock(死結)。


// 不好!會阻塞執行緒並造成問題
var result = GetDataAsync().Result;

async Task<string> GetDataAsync() { /* ... */ return "data"; }

正確做法是使用 await,不要阻塞執行緒。

錯誤 #3:非同步與 UI。
在圖形界面應用(WPF、WinForms)中,主要的問題是不要凍結 UI 執行緒。如果在主執行緒做長時間或阻塞的操作,整個應用會「卡住」。非同步能解決這個問題:把重工作放到背景去做,介面保持回應。

錯誤 #4:沒有統一的非同步命名慣例。
如果不在非同步方法名稱加上後綴 Async,很容易搞不清哪個方法是同步的、哪個是非同步的。這會導致意外阻塞和呼叫錯誤。請務必在非同步方法名稱末尾加上 Async

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION