C#語言async, await 簡單介紹與範例(入門級)

2023-06-21 18:01:27

      本文介紹非同步程式設計的基本思想和語法。在程式處理裡,程式基本上有兩種處理方式:同步和非同步。對於有些新手,甚至認為「同步」是同時進行的意思,這顯然是錯誤的。

同步的基本意思是:程式一個個執行方法,或者說在方法呼叫上,fun1(), fun2(), fun3(),fun4().. 按順序呼叫,而非同步的意思是:方法不是按順序執行,可能fun2執行的時間比較長

那就先執行fun3,fun4。等執行完了fun2 在執行後面的fun1,fun6,fun7..., 很顯然,非同步程式設計比同步程式設計複雜很多,因為他涉及到執行緒的同步。

 

      注意:對於單核CPU來說,任一時刻只能執行一條指令,對於這種微觀觀點,我們不用太過於深究,因為作業系統會幫助我們排程。換句話說,我們一邊列印word檔案,一邊聽歌,一邊寫字

雖然我們感覺是「同時」進行的,但是其實是CPU是在後臺不停的幫助我們切換程序,只是CPU切換的速度太快了,讓我們感覺我們是在「同時」做很多件事。

 

(一)基本非同步範例

下面程式碼演示了一個基本上非同步程式:(程式使用VS2022+.NET 7.0 開發的)

(1)HandleFileAsync() 表示這是一個非同步的方法,方法名稱前有一個 await 關鍵字。

 作為一個約定,方法總是以Async結尾,這樣,使用者看到這個方法就知道了這是一個非同步方法,這僅僅是方法名稱的一個約定,不加Async不影響使用。

(2)在HandleFileAsync 方法裡,模擬執行一些費時的操作。

(3)在HandleFileAsync 執行期間,不會阻塞主執行緒,現在輸入字串 123 ,系統會顯示出入的結果。

(4)在非同步方法執行完畢後,返回主執行緒,輸出計數的結果。

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

class Program
{
    public static void Main()
    {
        // Part 1: 開始處理大檔案檔案
        Task<int> task = HandleFileAsync();

        // 在檔案處理前,把控制權交給控制檯
        // 讓使用者輸入一些文字
        Console.WriteLine("請耐心等待,系統正在處理檔案," +" 但是此時,你可以輸入一些字母,回車後顯示");

        // 在檔案處理時,同時讀取你的輸入
        string line = Console.ReadLine();
        Console.WriteLine("你剛剛輸入的是: " + line);

        // Part 3: 等候處理結果
        //  顯示處理結果
        task.Wait();
        var x = task.Result;
        Console.WriteLine("計數: " + x);

        Console.WriteLine("程式執行完畢!");
        Console.ReadLine();
    }

    static async Task<int> HandleFileAsync()
    {
        string file = @"C:\qmx\token.txt";

        // Part 2: 下面開始處理大檔案

        Console.WriteLine("檔案處理開始");
        int count = 0;

        // 讀取檔案
        using (StreamReader reader = new StreamReader(file))
        {
            string v = await reader.ReadToEndAsync();

            // 處理資料
            count += v.Length;

           
            // 這裡是模擬程式碼,並沒有實際的意義, 讓程式執行1000萬次,
            // 純粹是模擬這是一個耗時的操作
            for (int i = 0; i < 1000000; i++)
            {
                int x = v.GetHashCode();
                if (x == 0)
                {
                    count--;
                }
            }
        }
        Console.WriteLine("檔案處理結束");
        return count;
    }
} 

 

 下面顯示的是執行結果

 

 當然上面後面的程式碼可以簡寫為  var x =await task.Result; 

 

 (二)執行緒阻塞(死鎖)

 在上面方法裡,必須小心的呼叫 Wait方法,因為處理不好,很容易發生任務阻塞。 Stephen Cleary 曾經給了一個典型的例子:見 

https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html

想象一下,我們有一個winForm應用程式,裡面有一個Button,在Button的點選事件裡,我們呼叫 HttpClient 的 GetStringAsync 方法獲取返回的JSON字串,然後把字串顯示在文字方塊裡。

為此,我們編寫了如下程式碼:

// Button的點選事件
public void Button1_Click(...)
{
//獲取Web返回的字串
  var jsonTask = GetJsonAsync(...);
//把字串顯示在文字方塊裡
  textBox1.Text = jsonTask.Result;
}


public static async Task<JObject> GetJsonAsync(Uri uri)
{
  // 呼叫 HttpClient 的 GetStringAsync 方法獲取JSON
  using (var client = new HttpClient())
  {
    var jsonString = await client.GetStringAsync(uri);
    return JObject.Parse(jsonString);
  }
}

現在你執行上面的程式碼,當你點選按鈕時,你會發現程式沒有出現你所想要的結果:因為程式被卡死了,根本無法進行其他操作。
除了終止應用程式,你別無選擇。為什麼會發生什麼死鎖現象呢? 

為了讓通俗解釋死鎖看下面一個例子:假設我們有一把藍鑰匙,可以開啟一扇藍色門;以及一把紅鑰匙,可以開啟一扇紅色門。兩把鑰匙被儲存在一個皮箱裡。同時我們定義六種行為:獲取藍鑰匙,開啟藍色門,歸還藍鑰匙,獲取紅鑰匙,開啟紅色門,歸還紅鑰匙。如下圖:你可以把6個行為理解為函數裡6個方法 (以下內容改寫自知乎

但是,當非同步調動時,每個方法順序就不那麼確定了,就可能出現如下這個情況

 

可以看到,當兩個執行緒都執行到第三步的時候,執行緒A在等執行緒B歸還紅鑰匙,執行緒B在等執行緒A歸還藍鑰匙,因而兩個執行緒都永遠卡在那裡無法前進。
這就是形成了死鎖。

理解了上面的死鎖,回頭再來看為什麼winForm裡產生了死鎖,主執行緒呼叫非同步方法返回的結果,被告知方法未完成,因此主執行緒在等待方法完成。

當非同步方法完成後,把自己狀態告知主執行緒已經Compled時,但是主執行緒一直在繁忙狀態,他在等待任務完成,因此,發生了死鎖。

這告訴我們在非同步程式設計時,要特別需要注意死鎖的問題。作為一個簡單的解決方法:只要加一個await 非同步就可以了

public async void Button1_Click(...)
{
  var json = await GetJsonAsync(...);
  textBox1.Text = json;
}

這也就是大家常說「一路異到底」。(不要在同步方法裡呼叫非同步方法,要非同步呼叫非同步,一路異到底)

 

(三)ContinueWith

 

在現實世界裡,經常會發生在一個方法完成之後,在進行下一個方法的呼叫,
例如,在Button 事件裡 (1)非同步從網路獲取HTML原始碼。 (2)把原始碼寫入 C:\File.txt 裡

這就需要第二步驟需要在第一步完成之後執行,此時需要用到ContinueWith 方法。

下面的程式碼簡單演示了 ContinueWith  (其實,ContinueWith 這個方法的名字就已經很好的解釋了他的作用)

using System;
using System.Threading.Tasks;

class Program
{
    static void Main()
    {
        //呼叫10次非同步方法
        for (int i = 0; i < 10; i++)
        {
            Run2Methods(i);
        }
        //所有呼叫都是非同步
        Console.ReadLine();
    }

    static async void Run2Methods(int count)
    {
        // 在調動完後,呼叫 ContinueWith 繼續操作 
        int result = await Task.Run(() => GetSum(count))
            .ContinueWith(task => MultiplyNegative1(task));
        Console.WriteLine("Run2Methods 結果: " + result);
    }

    static int GetSum(int count)
    {
        //這裡模擬一些額外操作
        int sum = 0;
        for (int z = 0; z < count; z++)
        {
            sum += (int)Math.Pow(z, 2);
        }
        return sum;
    }

    static int MultiplyNegative1(Task<int> task)
    {
        //  這裡模擬對數位取其相反數
        return task.Result * -1;
    }
}

下面顯示了執行結果

 

上面簡單的介紹了非同步程式設計。