CodeGym /課程 /C# SELF /使用檔案非同步操作的好處( async/ <...

使用檔案非同步操作的好處( async/ await

C# SELF
等級 42 , 課堂 0
開放

1. 介紹

想像你的應用程式是一間咖啡廳。咖啡廳裡只有一位服務生(也就是我們的 主要執行緒)。當顧客(使用者)點一杯咖啡(某個操作),服務生就走進廚房做咖啡,做完才回到座位準備接受下一個點餐。

現在想像有人點了……羅宋湯。不是普通的羅宋湯,而是一大鍋要煮兩個小時的羅宋湯!服務生會怎麼做?他會在爐子旁站兩個小時,什麼也不做,只是不停等待湯煮好。其他顧客會坐著揮手、不滿,但服務生根本看不到他們。咖啡廳「凍住」了,因為服務生被阻塞了。

在程式設計裡,這叫做 阻塞操作。當你呼叫一般的方法去讀寫檔案(例如 FileStream.Read()StreamReader.ReadLine()),你目前的執行緒會被 阻塞。它會停止執行其他所有程式碼,直到 I/O 操作結束。

看個簡單範例:


// 建立一個「大」檔案供示範
string largeFilePath = "LargeOrder.txt";
using (StreamWriter sw = new StreamWriter(largeFilePath))
{
    for (int i = 0; i < 1000000; i++) // 1 百萬 行
        sw.WriteLine($"第 {i} 行:某些非常重要的資訊...");
} // 檔案在這裡關閉,以便可以讀取

// !!! 注意:這是阻塞操作 !!!
string content = File.ReadAllText(largeFilePath);

執行這段程式。你會看到在寫入與讀取檔案的期間程式「卡住」。主控台不會接受輸入,也不會輸出新的訊息,直到檔案完全讀取完畢。之後才會繼續執行下一行程式。

在小型的主控台程式裡這可能不那麼明顯,但想像一下:

  • 有圖形介面的應用程式 (UI): 你按下「載入檔案」按鈕,程式視窗就「凍住」了。視窗不能移動、其他按鈕按不動、選單不回應。這會讓使用者體驗很糟糕。
  • 網頁伺服器: 伺服器在處理使用者請求。如果某個請求需要讀取非常大的檔案,那麼其他所有請求都會在隊列中等候,直到那個執行緒釋放。這會造成大量延遲與可擴展性低下。

這就是為什麼我們需要非同步!

2. 使用檔案非同步操作的好處

非同步不是為了讓磁碟變快。磁碟的速度還是那樣。非同步的重點在於:不要浪費時間等待慢速操作完成,而是把執行緒釋放出來去做其他事情

回到咖啡廳的比喻。現在服務生變聰明且會切換工作。當顧客點了「超大鍋羅宋湯」(讀大檔案)時,服務生不會站在爐子旁邊等。他把湯放著煮,然後回到廳裡繼續接單、收碗盤、服務其他顧客。等廚房叫湯好了,他再回去拿湯端給顧客。

關鍵差別:服務生不再被阻塞等待湯。相反地,他利用這段時間去做其他有用的事。

應用程式回應性 (User Interface Responsiveness)

這可能是對大多數桌面或行動應用最明顯也最重要的優點。如果你的程式做的事很久(讀檔、從網路下載資料、處理大量資訊),非同步方式可以讓你:

  • 保持 UI 互動性: 使用者可以繼續按按鈕、移動視窗、查看其他資訊,後台還在進行耗時操作。
  • 顯示進度指示: 你可以秀出漂亮的載入動畫或進度條,讓使用者知道程式沒有當掉,而是在工作中。

想像不是在服務生讀檔時主控台「凍住」,而是你看到他還在接其他「訂單」(例如顯示其他訊息或對使用者輸入做出反應),而檔案讀取在「背景」悄悄進行。這真的是好很多。

有效利用資源與可擴展性

這對伺服器應用(例如網路服務、API、後端)尤其重要,它們要同時服務大量使用者。

  • 不浪費執行緒: 在同步模型中,每個「耗時」的使用者請求都會佔用一個執行緒。如果你有 1000 個這樣的請求,你就需要 1000 個執行緒。每個執行緒會消耗記憶體與 CPU 資源,作業系統還要花時間在執行緒切換上。非同步允許同一個執行緒在等待 I/O 時去處理另一個請求。I/O 完成時,它再回到原本的請求。
  • 釋放 CPU: 當資料從磁碟或網路讀取時,CPU 很多時間是閒置的。非同步讓它在等待時去做其他有意義的運算。
  • 更少記憶體: 活躍執行緒少,伺服器記憶體消耗也會降低。

3. 簡化程式碼(在 C# 用 async/await

過去寫非同步程式碼很麻煩、冗長且容易出錯。你得手動管理執行緒、callback 和同步機制。那感覺就像在黑暗中用 LEGO 拼太空站。

但在 C# 出現了 asyncawait。它們像變魔法一樣,讓你寫非同步程式碼幾乎和同步程式碼一樣簡潔可讀。你只要告訴編譯器:「這裡可能會很慢,等一下,但別把整個世界鎖住」。


// 這是範例,示範非同步程式碼長什麼樣子(暫時不深入說明)
// 在後面的講義裡我們會詳細拆解!

public static async Task Main(string[] args) // 這就是非同步 Main 的樣子
{
    Console.WriteLine("程式開始以非同步方式讀取一個非常大的檔案...");

    Stopwatch stopwatch = Stopwatch.StartNew();
    
    // !!! 注意:這是非同步操作!!!
    await File.ReadAllTextAsync(largeFilePath); // 這不會阻塞執行緒
    
    stopwatch.Stop();

    Console.WriteLine($"檔案已讀取!耗時:{stopwatch.ElapsedMilliseconds} 毫秒。");
    // 在檔案讀取時,這裡可以做其他工作!
}

注意:非同步不會讓磁碟層級的 I/O 更快。如果讀取 1 GB 檔案需要 15 秒,那仍然是 15 秒。但差別在於,你的處理器和執行緒在這些秒數裡在做什麼。同步情況下它們是在空轉,非同步則讓它們在等待時去執行其他工作。

簡單整理如下:

特性 同步操作 (普通 Read/Write) 非同步操作 (ReadAsync/WriteAsync)
執行緒狀態 被阻塞,直到操作完成 不阻塞,執行緒可被釋放去處理其他工作
UI 回應性 應用程式會「凍住」 應用程式保持互動
CPU 使用 在等待 I/O 時閒置 在等待 I/O 時可以做其他工作
可擴展性 低(同時操作需要大量執行緒) 高(少量執行緒能處理大量請求)
撰寫難度 簡單 過去很複雜,但 async/await 大幅簡化
I/O 本身的速度 不加速 不加速(速度取決於磁碟)
何時使用? 快速、短暫的操作 任何可能會很久的操作(I/O、網路、資料庫)

總之,非同步是讓你的應用程式更有 回應性 與更易於 擴展 的一種方式,特別是當你要跟外部慢速資源互動時(像是檔案系統或網路)。它不是取代緩衝(buffering),而是與之互補。緩衝會加速資料搬運本身,非同步則保證你的程式在搬運時不會站著不動。

4. 幕後運作

真實世界例子:影片編輯器、遊戲、網站

幾乎所有處理大型檔案的現代應用都採用非同步方式。主流媒體播放器在載入影片時不會鎖住介面。伺服器在一個客戶下載大檔案時不會整個當掉。連備份程式或雲端同步工具也都「在背景」處理,讓使用者可以同時做其他事。

簡單說明它是如何運作的

.NET 裡的非同步檔案方法(例如 ReadAsync, WriteAsync)實際上利用作業系統的能力,讓程式執行緒在長時間的 I/O 操作時不被阻塞。這是透過系統呼叫告訴 OS:「幫我讀這個檔案,完成時再通知我」來實現的。

視覺化:非同步讀取是怎麼運作的(示意圖)

sequenceDiagram
    participant UserCode as 你的程式碼
    participant OS as 作業系統
    participant Disk as 磁碟

    UserCode->>OS: 非同步讀取檔案的請求
    OS->>Disk: 讀取資料
    UserCode->>UserCode: 繼續執行其他任務
    OS->>OS: 等待讀取完成
    Disk-->>OS: 資料準備好
    OS-->>UserCode: 通知讀取完成
    UserCode->>UserCode: 處理資料

5. 哪些情況下非同步最有幫助

  • 有圖形介面的應用(避免 UI 被鎖住)。
  • 處理大量並發檔案請求的伺服器。
  • 自動化處理大量資料的腳本(例如備份)。
  • 與慢速或網路磁碟交互的工具。

資料越大、儲存媒體越慢,非同步的優勢越明顯。即使你不是在開發大型應用,養成使用 Async 方法的習慣也很有用:它們是現代 C# 的一項標準能力。

現在你知道為什麼用檔案做非同步不是流行用語,而是建立快速且回應良好的 .NET 9 應用的一項重要技巧。接下來的講義我們會深入語法、實作,以及常見的使用場景(例如 ReadAsyncWriteAsync 等),開始寫我們的第一個真正的非同步程式碼!別走開,會很有趣!

6. 非同步性

我們會在第 55-62 級深入探討非同步的運作細節。現在我只是想先讓你對它有個初步認識。如果你在這一級完全沒看懂也沒關係。 可以先跳過這些講義和題目,等你對非同步有更多基礎後再回來學。

平行(Parallelism) vs 非同步(Asynchrony)

如果你在電腦上同時開好幾個 Windows 應用,且它們同時做事,程式設計師會說那些工作是 平行執行 的。

如果你把手機遊戲縮到背景去做別的事,而那個遊戲暫停了,這比較像是 非同步工作。非同步不是強調同時執行,而是強調 同時等待

範例

假設你要做家事。你啟動洗碗機,然後在它運作時你把衣服放進洗衣機。洗衣機洗的時候,你把派放進烤箱。你一個人,但是可以同時做很多事,因為你在某些事情等待時會切換去做別的事,而不是一直等它完成。

當洗衣機洗好會通知你,你再回去處理洗衣機的事情。從洗衣機的角度看,你好像只是等它完成,但其實你一直在做其他事。

你不能同時裝洗碗機、把派放進去,並從洗衣機拿衣服出來。但當某樣事情忙碌時,你不需要只是空等,可以利用這段時間做其他有用的事。

重要提醒

如果你只有一個任務,那麼「等任務完成」和「在等待時做別的事」看起來沒差別。但如果任務很多,兩者的差異會非常明顯。

7. 常見錯誤與陷阱

最常見的誤解是:只要呼叫非同步方法,一切就會神奇地變快。實際上,如果使用不當(例如忘記 await),程式會變得難以預測:結果還沒準備好,你的程式就開始使用它。在圖形應用中,事件處理通常應該是非同步的,否則介面會「卡住」。

另一點: 非同步不是讓讀寫更快,而是讓你的程式在面對慢速操作時能保持高效與回應性。

下一步?

現在我們已經了解了為什麼需要非同步,下一步就是學會怎麼用。在下一堂我們會深入非同步讀寫檔案的語法,介紹非同步方法的版本(例如 ReadAsyncWriteAsync),並開始寫第一個真正的非同步程式碼!很有趣,別走開!

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