⚡ Async / Await 非同步程式設計
📌 為什麼需要非同步?
想像你是一個餐廳服務生:
同步(Synchronous): 你幫客人 A 點餐 → 站在廚房門口等菜做好 → 上菜 → 然後才去服務客人 B。 (效率很差!等菜的時間完全浪費了)
非同步(Asynchronous): 你幫客人 A 點餐 → 把單子送進廚房 → 不等了,先去服務客人 B → 廚房做好了再回來上菜。 (效率高!等待的時間可以做別的事)
// 同步版本 — 一個一個等
void SyncExample() // 同步方法
{
Console.WriteLine("開始下載檔案 A...");
Thread.Sleep(3000); // 模擬等待 3 秒(什麼事都不能做!)
Console.WriteLine("檔案 A 下載完成");
Console.WriteLine("開始下載檔案 B...");
Thread.Sleep(3000); // 再等 3 秒
Console.WriteLine("檔案 B 下載完成");
// 總共花了 6 秒
}
// 非同步版本 — 同時進行
async Task AsyncExample() // async 標記這是非同步方法
{
Console.WriteLine("開始下載檔案 A...");
Task taskA = Task.Delay(3000); // 開始等待但不阻塞
Console.WriteLine("開始下載檔案 B...");
Task taskB = Task.Delay(3000); // 同時開始等待
await Task.WhenAll(taskA, taskB); // 等兩個都完成
Console.WriteLine("兩個檔案都下載完成!");
// 總共只花了 3 秒(同時進行!)
}
📌 async / await 關鍵字
// async 放在方法宣告前面,表示這是一個非同步方法
// await 放在非同步操作前面,表示「等這個做完再繼續」
// 非同步方法的回傳型別
async Task DoSomethingAsync() // 不回傳值用 Task
{
await Task.Delay(1000); // 等待 1 秒(非阻塞)
Console.WriteLine("做完了!"); // 1 秒後執行
}
async Task<int> GetNumberAsync() // 回傳 int 用 Task<int>
{
await Task.Delay(500); // 等待 0.5 秒
return 42; // 回傳結果
}
async Task<string> GetGreetingAsync(string name) // 回傳 string 用 Task<string>
{
await Task.Delay(100); // 模擬非同步操作
return $"你好,{name}!"; // 回傳問候語
}
// 呼叫非同步方法
async Task Main() // Main 方法也可以是 async
{
await DoSomethingAsync(); // 等待完成
int number = await GetNumberAsync(); // 等待並取得結果
Console.WriteLine(number); // 輸出:42
string greeting = await GetGreetingAsync("小明"); // 等待並取得結果
Console.WriteLine(greeting); // 輸出:你好,小明!
}
📌 Task 與 Task
// Task 代表一個「正在進行的工作」
// 就像你在餐廳點了餐,拿到一張號碼牌(Task)
// 號碼牌本身不是食物,但你可以用它來等食物做好
// 建立 Task 的方式
Task task1 = Task.Run(() => // 用 Task.Run 在背景執行
{
Console.WriteLine("背景工作開始"); // 在背景執行緒上跑
Thread.Sleep(1000); // 模擬耗時操作
Console.WriteLine("背景工作完成");
});
Task<int> task2 = Task.Run(() => // 有回傳值的 Task
{
int sum = 0; // 計算總和
for (int i = 0; i < 1000000; i++) // 大量計算
{
sum += i; // 累加
}
return sum; // 回傳結果
});
await task1; // 等待 task1 完成
int result = await task2; // 等待 task2 完成並取得結果
Console.WriteLine($"計算結果:{result}"); // 印出結果
📌 Task.WhenAll 與 Task.WhenAny
// Task.WhenAll — 等所有工作都完成
async Task DownloadAllAsync() // 同時下載多個檔案
{
Task<string> file1 = DownloadFileAsync("file1.txt"); // 開始下載 1
Task<string> file2 = DownloadFileAsync("file2.txt"); // 開始下載 2
Task<string> file3 = DownloadFileAsync("file3.txt"); // 開始下載 3
// WhenAll 等所有 Task 都完成,回傳所有結果
string[] results = await Task.WhenAll(file1, file2, file3);
foreach (string r in results) // 走訪所有結果
{
Console.WriteLine(r); // 印出每個下載結果
}
}
// Task.WhenAny — 等任何一個工作完成就好
async Task GetFastestAsync() // 取得最快的回應
{
Task<string> server1 = FetchFromServerAsync("https://server1.com");
Task<string> server2 = FetchFromServerAsync("https://server2.com");
// WhenAny 只要有一個完成就回傳
Task<string> fastest = await Task.WhenAny(server1, server2);
string result = await fastest; // 取得最快完成的結果
Console.WriteLine($"最快回應:{result}"); // 使用最快的結果
}
// 模擬下載檔案
async Task<string> DownloadFileAsync(string fileName)
{
var random = new Random();
int delay = random.Next(500, 2000); // 隨機延遲 0.5-2 秒
await Task.Delay(delay); // 模擬下載時間
return $"{fileName} 下載完成(耗時 {delay}ms)"; // 回傳結果
}
// 模擬從伺服器取得資料
async Task<string> FetchFromServerAsync(string url)
{
var random = new Random();
await Task.Delay(random.Next(100, 1000)); // 模擬網路延遲
return $"來自 {url} 的回應"; // 回傳回應
}
📌 HttpClient 非同步範例
// HttpClient 是 .NET 內建的 HTTP 客戶端
// 所有 HTTP 操作都是非同步的
async Task FetchWebPageAsync() // 抓取網頁內容
{
// 建議用 static 或 DI 管理 HttpClient(不要每次都 new)
using HttpClient client = new HttpClient(); // 建立 HTTP 客戶端
try // 網路操作可能失敗,要用 try-catch
{
// GetStringAsync 是非同步方法
string content = await client.GetStringAsync("https://api.github.com");
Console.WriteLine($"取得 {content.Length} 個字元"); // 印出內容長度
// 也可以取得完整的 HttpResponseMessage
HttpResponseMessage response = await client.GetAsync("https://api.github.com");
if (response.IsSuccessStatusCode) // 檢查是否成功(200-299)
{
string body = await response.Content.ReadAsStringAsync(); // 讀取內容
Console.WriteLine(body); // 印出回應內容
}
else
{
Console.WriteLine($"請求失敗:{response.StatusCode}"); // 印出錯誤狀態碼
}
}
catch (HttpRequestException ex) // 捕捉 HTTP 相關的例外
{
Console.WriteLine($"網路錯誤:{ex.Message}"); // 印出錯誤訊息
}
}
// POST 請求範例
async Task PostDataAsync() // 送出資料
{
using HttpClient client = new HttpClient(); // 建立客戶端
var data = new { name = "小明", age = 20 }; // 要送出的資料
string json = System.Text.Json.JsonSerializer.Serialize(data); // 轉成 JSON
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
HttpResponseMessage response = await client.PostAsync("https://api.example.com/users", content);
string result = await response.Content.ReadAsStringAsync(); // 讀取回應
Console.WriteLine(result); // 印出結果
}
📌 ConfigureAwait(false)
// ConfigureAwait 控制 await 之後要在哪個執行緒繼續
// 預設:await 之後回到原來的執行緒(UI 執行緒)
async Task UpdateUIAsync() // UI 應用程式中使用
{
string data = await GetDataAsync(); // 預設會回到 UI 執行緒
// label.Text = data; // 可以安全地更新 UI
}
// ConfigureAwait(false):不需要回到原來的執行緒
async Task<string> GetDataFromApiAsync() // 程式庫 / 非 UI 程式碼
{
using HttpClient client = new HttpClient();
// ConfigureAwait(false) 告訴系統:我不需要回到原來的執行緒
// 這在程式庫中可以避免死鎖,也能提升效能
string result = await client.GetStringAsync("https://api.example.com")
.ConfigureAwait(false); // 不需要回到原來的 context
return result; // 處理結果
}
// 何時用 ConfigureAwait(false):
// ✅ 在程式庫(Library)中 — 幾乎都要用
// ✅ 在非 UI 的應用程式中
// ❌ 在 UI 事件處理器中不要用(否則不能更新 UI)
// ❌ 在 ASP.NET Core 中通常不需要(已經沒有 SynchronizationContext)
🤔 我這樣寫為什麼會錯?
❌ 錯誤:async void(最常見的錯誤!)
// ❌ 錯誤:不要用 async void(除了事件處理器)
async void BadMethod() // async void 無法被 await
{
await Task.Delay(1000);
throw new Exception("這個例外無法被捕捉!"); // 會直接崩潰!
}
// ✅ 正確:用 async Task
async Task GoodMethod() // async Task 可以被 await
{
await Task.Delay(1000);
throw new Exception("這個例外可以被 try-catch 捕捉");
}
// 唯一可以用 async void 的地方:UI 事件處理器
// async void Button_Click(object sender, EventArgs e) { ... }
❌ 錯誤:死鎖(Deadlock)
// ❌ 錯誤:在同步方法中用 .Result 或 .Wait() 等待非同步方法
void DeadlockExample() // 同步方法
{
// 在 UI 或 ASP.NET (非 Core) 中,這會造成死鎖!
// var result = GetDataAsync().Result; // ❌ 死鎖!
// GetDataAsync().Wait(); // ❌ 死鎖!
}
// ✅ 正確:一路 async/await 到底("async all the way")
async Task CorrectExample() // 非同步方法
{
var result = await GetDataAsync(); // ✅ 用 await
Console.WriteLine(result);
}
// 如果真的必須在同步中呼叫非同步方法:
void WorkaroundExample()
{
// 用 Task.Run 包裝(避免在原來的 context 上等待)
var result = Task.Run(() => GetDataAsync()).Result;
}
❌ 錯誤:忘記 await
// ❌ 錯誤:忘記 await,方法會立刻回傳,不等結果
async Task ForgotAwait()
{
// Task.Delay(5000); // ❌ 沒有 await,不會等 5 秒
// 上面的 Task.Delay 被忽略了,程式直接往下跑
await Task.Delay(5000); // ✅ 有 await,會等 5 秒
}
❌ 錯誤:在迴圈中逐一 await
// ❌ 效率差:一個一個等
async Task SlowVersion()
{
var urls = new[] { "url1", "url2", "url3" };
foreach (var url in urls)
{
await FetchFromServerAsync(url); // 等完一個才做下一個
}
// 如果每個要 1 秒,總共要 3 秒
}
// ✅ 效率好:同時進行
async Task FastVersion()
{
var urls = new[] { "url1", "url2", "url3" };
var tasks = urls.Select(url => FetchFromServerAsync(url)); // 同時開始
await Task.WhenAll(tasks); // 等所有完成
// 如果每個要 1 秒,總共只要 1 秒(同時進行)
}