iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
佛心分享-SideProject30

30天的旅程!從學習C#到開發小專案系列 第 13

DAY 13 - C#中的非同步程式設計 (Asynchronous Programming)

  • 分享至 

  • xImage
  •  

哈囉大家好!
今天要介紹在撰寫程式碼中很常用到的「非同步程式設計」(Asynchronous Programming)!

C#中的非同步程式設計(Asynchronous Programming),可以讓程式在執行時操作時,不必停下來等待結果,可以釋放執行緒(Thread)去處理其他的任務。

async和await

C# 透過asyncawait兩個關鍵字來撰寫非同步程式碼,稱為工作基礎非同步非同步模式(Task-based Asynchronous Pattern)。

  1. async

    • 用來標記一個function是非同步function。
    • 非同步function通常會回傳Task(沒有回傳值)或Task<T>(有回傳值T)來代表一個進行中的非同步操作
  2. await

    • 只能在async function中使用。
    • 用來等待一個Task完成
    • 當程式碼執行到await時,如果該Task還沒有完成,程式會暫停function, 並將控制權交給caller,這時執行緒會被釋放。
    • 當等待的Task完成時,程式會從await之後的位置自動恢復執行。

比較「同步」和「非同步」的差異

這裡我用「家庭主婦媽媽」來比喻執行緒(Thread)處理任務的過程:

1.同步(Synchronous)
有一位家庭主婦媽媽(一個執行緒),需要完成三項任務:洗衣服、烤土司、泡咖啡。
媽媽早上的任務流程如下:

  • 開始洗衣服(需要1個半小時)
  • 媽媽站在旁邊等衣服洗好拿去晾乾 (執行緒被阻塞)
  • 衣服晾好之後,媽媽開始烤土司(需要10分鐘)
  • 媽媽站在那裡等土司烤好 (執行緒被阻塞)
  • 土司烤好了,媽媽開始泡咖啡(需要5分鐘)
  • 媽媽站在那裡等咖啡泡好 (執行緒被阻塞)
    總共花了1小時45分鐘,大部分的時間都用來等待任務完成,但媽媽(執行緒)在等待的時候無法做任何事。
  1. 非同步(Asynchronous)
    媽媽改成採用「非同步方式」執行任務:
  • 媽媽立刻把衣服丟入洗衣機,倒入洗衣劑按下啟動按鈕(Task啟動)
  • 媽媽:「當衣服洗好時,請通知我」,然後跑去做下一個任務-烤土司 (執行緒被釋放)
  • 媽媽立刻打開電磁爐、倒油並把蛋打入 (Task啟動)
  • 媽媽:「當蛋煮熟時,請通知我」,然後立刻跑去做下一個任務-泡咖啡
  • 當蛋煎好時(Task完成),通知系統(爐子發出聲音),媽媽回來把蛋盛裝,並且回去繼續泡咖啡。
  • 當咖啡煮好時(Task完成),媽媽將咖啡倒入杯中。
  • 最後媽媽等到衣服洗好(洗衣機發出逼逼聲,Task完成),媽媽將衣服拿去晾乾。
    所有的任務同時開始,所以總共花費的時津取決於時間最長的任務(洗衣服)。媽媽(執行緒)幾乎沒有閒置等待的時候,因為一直輪轉於不同任務之間。

角色名詞對應:

  • 媽媽/執行緒Thread: 負責執行程式碼指令
  • 洗衣服,煎蛋,泡咖啡/Task: 代表一個耗時的非同步操作
  • 當...時,請通知我/await: 讓執行緒在等待通知的過程可以去做其他的任務,不會進行閒置的等待。

非同步的優點

  1. 響應性:非同步操作讓使用者在使用應用程式時,防止UI(使用者介面)在等待網路回應時凍結,提高使用者體驗。
  2. 可擴展性:釋放執行緒代表伺服器可以處理更多併發的使用者請求,不需要因為等待而浪費執行緒。

實際撰寫非同步function

這裡用一個情境例子來練習:我要將log寫入特定文字檔中,要透過async function來提高應用程式的響應性(避免在寫入log時因為寫入大檔案或共享資料夾,同步設計造成UI介面暫時停止響應):

using System;
using System.IO;
using System.Threading;

public class LogProcessor
{
    private const string LogFilePath = "my_example_log.txt";

    public async Task WriteLogAsync(string message)
    {
        string logEntry = $"{DateTime.Now:HH:mm:ss} [Thread ID: {Thread.CurrentThread.ManagedThreadId}] - {message}";
        Console.WriteLine($"準備寫入日誌: {logEntry}");
        await Task.Delay(5000); // await 5秒

        await File.AppendAllTextAsync(LogFilePath, logEntry + Environment.NewLine);

        Console.WriteLine("日誌寫入完成");
    } 
}

public class Program
{
    public static async Task Main(string[] args)
    {
        LogProcessor processor = new LogProcessor();
        
        Console.WriteLine("main:啟動日誌寫入。");

        await processor.WriteLogAsync("使用者登入事件"); 

        Console.WriteLine("main:寫入完成,執行其他工作");
    }
}

程式執行流程如下:

  1. 會馬上印出「main:啟動日誌寫入」。
  2. 遇到await Task.Delay(5000)時,會把控制權返回給caller(Main function)>
  3. Main function會立刻執行並印出「main:寫入完成,執行其他工作」。
  4. 大約5秒後,將訊息寫入日誌後,會印出「日誌寫入完成」。

上一篇
DAY 12 - 利用EF Core來操作資料庫
下一篇
DAY 14 - 讓程式碼更乾淨:ASP.NET Core中的服務層(Service Layer)和依賴注入(DI)
系列文
30天的旅程!從學習C#到開發小專案15
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言