2017年10月15日 星期日

C# 非同步程式設計的 async void 與 async Task 的差異

若你正在觀看此篇文章,那麼你將會對於 為什麼需要使用非同步程式設計,真的可以提升整體應用程式的執行效能嗎? 問題更有興趣的。

今天,想來談談絕大部分的 C# 開發者都會產生這樣的不正確的 C# 程式碼寫法與用法,那就是當我們採用 工作式非同步模式 (TAP, Task-based Asynchronous Pattern) 進行非同步的程式開發的時候,並且想要寫個非同步運作的分法,在很多時候,您會想說,底下的方法,就是一個非同步運作的程式碼方法。

了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式
了解更多關於 [Thread Class] 的使用方式
了解更多關於 [Task Class] 的使用方式


在這裡,我們在方法 M1 前面使用了 async 修飾詞,不過,這個方法並沒有任何物件值需要回傳,因此,在底下的程式碼中,您看到了這個方法使用了 async void
在工作式非同步模式的方法中,回傳值的類型僅有 Task / Task<T>,前者代表沒有任何實際物件要回傳,而後者表示要回傳型別為 T 的物件;不過,那我們為什麼又可以使用 void 這個關鍵字呢?通常,我們會在綁定事件的方法上,需要使用 async void 這樣的標示,這是因為這些事件的函式簽章就是僅支援 void 這樣的回傳值,另外,我們在這個綁定的事件方法中,又需要撰寫工作式非同步模式的功能,因此,又需要在這個事件方法前面,加入 async 這個修飾詞。
所以,除了是在綁定事件的方法內,其他任何情況下,對於您開發的工作式非同步模式方法,就不需要使用async void這樣的回傳值標示。
上面的說明,很重要,很重要,很重要
public async void M1()
{
    var foo = DateTime.UtcNow.ToString("mm:ss.ffff");
    Console.WriteLine($"M1 開始時間 : {foo}");
    await Task.Delay(3000);
    foo = DateTime.UtcNow.ToString("mm:ss.ffff");
    Console.WriteLine($"M1 結束時間 : {foo}");
}
現在,讓我們來看看這個方法實際使用的情況,首先,我們定義一個類別 A,它的定義如下程式碼,其中, M1 是我們使用 async void 的方式建立的方法,而 M2,則是正常的工作式非同步模式方法的程式碼寫法。
class A
{
    public async void M1()
    {
        var foo = DateTime.UtcNow.ToString("mm:ss.ffff");
        Console.WriteLine($"M1 開始時間 : {foo}");
        await Task.Delay(3000);
        foo = DateTime.UtcNow.ToString("mm:ss.ffff");
        Console.WriteLine($"M1 結束時間 : {foo}");
    }

    public async Task M2()
    {
        var foo = DateTime.UtcNow.ToString("mm:ss.ffff");
        Console.WriteLine($"M2 開始時間 : {foo}");
        await Task.Delay(3000);
        foo = DateTime.UtcNow.ToString("mm:ss.ffff");
        Console.WriteLine($"M2 結束時間 : {foo}");
    }
}
M1這個方法,會先顯示這個方法的開始執行時間,接著,便會暫停3秒鐘,接著便會顯示結束執行這個方法的時間。
在測試程式碼中,我們先建立一個類別A的物件 objA,接著呼叫 objA.M1() 方法,最後,等候使用按下任一按鍵。
class Program
{
    static async Task Main(string[] args)
    {
        A objA = new A();

        objA.M1();
        Console.WriteLine($"Press any key to Exist...{Environment.NewLine}");
        Console.ReadKey();
    }
}
上面的程式碼執行結果如底下所示,你可以看到,當呼叫 M1 方法之後,接著, M1 方法似乎就以另外一個執行緒的方式進行執行,所以, Main 這個方法內的程式碼,沒有等到 M1 執行完畢,就直接執行 objA.M1() 之後的所有敘述,而當三秒鐘之後,就會顯示 M1 方法已經結束的時間。
這是因為我們使用了 async void 的方式來宣告這個方法,因此,我們沒有任何方法,可以等候 M1 方法執行完成之後,才要繼續執行 objA.M1() 之後的所有敘述。
M1 開始時間 : 09:23.1819
Press any key to Exist...

M1 結束時間 : 09:26.2171
現在,讓我們修改測試程式碼,我們在呼叫 M1 方法前後,與在 M1 方法內,當要呼叫 await Task.Delay(3000); 的前後,都顯示出當時執行緒的 ID (這裡顯示的是受管理的執行緒ID) Thread.CurrentThread.ManagedThreadId
class A
{
    public async void M1()
    {
        var foo = DateTime.UtcNow.ToString("mm:ss.ffff");
        Console.WriteLine($"M1 開始時間 : {foo}");
        Console.WriteLine($"M1 {Thread.CurrentThread.ManagedThreadId}");
        await Task.Delay(3000);
        Console.WriteLine($"M1 {Thread.CurrentThread.ManagedThreadId}");
        foo = DateTime.UtcNow.ToString("mm:ss.ffff");
        Console.WriteLine($"M1 結束時間 : {foo}");
    }

    public async Task M2()
    {
        var foo = DateTime.UtcNow.ToString("mm:ss.ffff");
        Console.WriteLine($"M2 開始時間 : {foo}");
        await Task.Delay(3000);
        foo = DateTime.UtcNow.ToString("mm:ss.ffff");
        Console.WriteLine($"M2 結束時間 : {foo}");
    }
}
class Program
{
    static async Task Main(string[] args)
    {
        A objA = new A();

        Console.WriteLine($"Main {Thread.CurrentThread.ManagedThreadId}");
        objA.M1();
        Console.WriteLine($"Main {Thread.CurrentThread.ManagedThreadId}");
        Console.WriteLine($"Press any key to Exist...{Environment.NewLine}");
        Console.ReadKey();
    }
}
此時,執行結果將會如下所示,您會看到,當 M1 的方法內,其 await Task.Delay(3000); 敘述執行完畢之後,此時的執行緒 ID 變成了 4。
Main 1
M1 開始時間 : 20:55.2604
M1 1
Main 1
Press any key to Exist...

M1 4
M1 結束時間 : 20:58.2897
最後,讓我們加入 M2 的方法呼叫,對於要呼叫 M2 的非同步方法,我們使用了 await 關鍵字 await objA.M2();
class A
{
    public async void M1()
    {
        var foo = DateTime.UtcNow.ToString("mm:ss.ffff");
        Console.WriteLine($"M1 開始時間 : {foo}");
        Console.WriteLine($"M1 {Thread.CurrentThread.ManagedThreadId}");
        await Task.Delay(3000);
        Console.WriteLine($"M1 {Thread.CurrentThread.ManagedThreadId}");
        foo = DateTime.UtcNow.ToString("mm:ss.ffff");
        Console.WriteLine($"M1 結束時間 : {foo}");
    }

    public async Task M2()
    {
        var foo = DateTime.UtcNow.ToString("mm:ss.ffff");
        Console.WriteLine($"M2 開始時間 : {foo}");
        await Task.Delay(1000);
        foo = DateTime.UtcNow.ToString("mm:ss.ffff");
        Console.WriteLine($"M2 結束時間 : {foo}");
    }
}
class Program
{
    static async Task Main(string[] args)
    {
        A objA = new A();

        Console.WriteLine($"Main {Thread.CurrentThread.ManagedThreadId}");
        objA.M1();
        await objA.M2();
        Console.WriteLine($"Main {Thread.CurrentThread.ManagedThreadId}");
        Console.WriteLine($"Press any key to Exist...{Environment.NewLine}");
        Console.ReadKey();
    }
}
底下將為這個測試程式碼的執行輸出結果,我們可以看到, objA.M1() 方法一執行到 await Task.Delay(3000);敘述之後,就立即返回到 Main 方法內,接著執行 await objA.M2();,不過,此時,整個程式將會等候到 objA.M2() 方法執行完後,才會繼續進行下去,而在這個時候, M1 的方法,也持續在等候 Task.Delay 的甦醒時間
ain 1
M1 開始時間 : 24:53.7485
M1 1
M2 開始時間 : 24:53.7645
M2 結束時間 : 24:54.7670
Main 4
Press any key to Exist...

M1 5
M1 結束時間 : 24:56.7678



對於已經具備擁有 .NET / C# 開發技能的開發者,可以使用 Xamarin.Forms Toolkit 開發工具,便可以立即開發出可以在 Android / iOS 平台上執行的 App;對於要學習如何使用 Xamarin.Forms & XAML 技能,現在已經推出兩本電子書來幫助大家學這這個開發技術。
這兩本電子書內包含了豐富的逐步開發教學內容與相關觀念、各種練習範例,歡迎各位購買。
Xamarin.Forms 電子書
想要購買 Xamarin.Forms 快速上手 電子書,請點選 這裡

想要購買 XAML in Xamarin.Forms 基礎篇 電子書,請點選 這裡




沒有留言:

張貼留言