C# 中 async 和 await 的基本使用


C# 中 async 和 await 的基本使用

前言

經常在 C# 的代碼中看到以 Async 結尾的方法,大概知道意為異步方法,但不知道怎么使用,也不知道如何定義。

對於“同步”、“異步”、“阻塞”、"非阻塞"這幾個概念還是比較清楚的。同步是指等待方法的執行完成;異步是指設置方法執行后繼續其它操作,通過回調的方式對結果進行其它操作;阻塞是指執行到這一步就不往后了,直到執行完成;非阻塞是指執行這一步時,還可以進行其它操作。

這兩組概念其實是講的一個東西,只是針對的方向有些許區別(一個強調是否立即返回,一個強調是否繼續往后)

對於 C# 中的 async 和 await,可以這么簡單理解:async 告訴 runtime,這個函數可以異步去執行以提高效率。await 則告訴 runtime,真正耗時的是在我這個關鍵字后面的操作。

本文僅希望在使用的層面驗證,對於原理以及是否新開線程等,由於能力有限,暫不深入

思路與實驗

對於本地環境而言,讀取大文件是比較耗時的操作之一。因此先寫一個讀取文件的操作,再用 async 和 await 的方法將其包裹,以探究這兩個關鍵字的使用(為了模擬執行一番后得到最后的結果,我們返回二進制文件的最后一個字節所代表的數字)

1. 初步代碼,同步調用耗時方法

using System;
using System.IO;

namespace AsyncAwaitTest
{
    class Program
    {
        static void Main(string[] args)
        {
            DateTime time = DateTime.Now;
            byte targetNum;

            Console.WriteLine("模擬執行其它操作,用 A 表示");

            targetNum = ReadLargeFile();    // 為體現同步異步區別,執行三遍
            targetNum = ReadLargeFile();
            targetNum = ReadLargeFile();
            Console.WriteLine("最后一個字節所代表的數字為:" + targetNum);

            Console.WriteLine("模擬執行其它操作,用 B 表示");
            Console.WriteLine("耗時為:" + (DateTime.Now - time).Seconds);
            Console.ReadLine();
        }
        
        /// <summary>
        ///  讀取大文件(耗時方法)
        /// </summary>
        /// <returns></returns>
        private static byte ReadLargeFile()
        {
            const int BUFFER_SIZE = 4096;

            FileStream fileStream = new FileStream(
                                            Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Windows10.iso"), 
                                            FileMode.Open, FileAccess.Read, FileShare.Read);    // 在此處設置允許共享
            byte[] buffer = new byte[BUFFER_SIZE];
            int readOutCount = 0, lastReadOutCount = 0;
            while ((readOutCount = fileStream.Read(buffer, 0, BUFFER_SIZE)) != 0)
            {
                lastReadOutCount = readOutCount;
            }

            return buffer[lastReadOutCount - 1];
        }
    }
}

可以看出,耗時約 10 s。

2. 使用異步關鍵字包裹同步方法

新增函數 AsyncCallReadLargeFile,並修改 main 函數中的調用。通過查閱資料可以得知,Task 類的 Result 方法在執行時會阻塞

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

namespace AsyncAwaitTest
{
    class Program
    {
        static void Main(string[] args)
        {
	    ......

            Console.WriteLine("模擬執行其它操作,用 A 表示");	// 執行順序 1

            Task<byte> t1 = AsyncCallReadLargeFile();    // 為體現同步異步區別執行三遍
            Task<byte> t2 = AsyncCallReadLargeFile();
            Task<byte> t3 = AsyncCallReadLargeFile();

	    Console.WriteLine("模擬執行其它操作,用 C 表示"); // 執行順序 3 或 4

            targetNum = t1.Result;
            targetNum = t2.Result;
            targetNum = t3.Result;

            Console.WriteLine("最后一個字節所代表的數字為:" + targetNum);	// 執行順序 6

            Console.WriteLine("模擬執行其它操作,用 B 表示");	// 執行順序 7
            
	    ......
        }

        /// <summary>
        /// 使用異步關鍵字包裹同步方法
        /// </summary>
        /// <returns></returns>
        private static async Task<byte> AsyncCallReadLargeFile()
        {
	    Console.WriteLine("模擬執行異步子方法,用 a 表示");		// 執行順序 2	
	
            byte result = await Task.Run(ReadLargeFile);

	    Console.WriteLine("模擬執行異步子方法,用 b 表示");		// 執行順序 5
            return result;
        }

        /// <summary>
        /// 讀取大文件(耗時方法)
        /// </summary>
        /// <returns></returns>
        private static byte ReadLargeFile()
        {
	    Console.WriteLine("讀取文件");	// 執行順序 3 或 4
            ......
        }
    }
}

小結:通過耗時可以明顯看出:
(1)我們的異步方法確實是以異步的方式執行了(對同一文件進行三個異步讀操作,耗時沒有疊加)
(2)大致的執行順序如代碼注釋中所示,也即,使用 await 時,確實等待執行完成當前后才會執行異步函數中后續的方法
(3)即使在異步函數中,未用 await 修飾的方法也是同步執行的(通過截圖無法看出,但通過觀察代碼輸出可以看出)

其它一些思考

1. 異步的方法最終會由同步方法調用

這句話看上去有點絕對了,但確實是這個道理。從寫法上:寫函數時,有 async 就必須有 await(否則會警告,並且以同步方式執行),有 await 就必須有 async(否則會報錯),而異步函數必須要使用這兩個成對出現的關鍵字。從道理上:異步方法就是來解決同步方法順序執行過於循規蹈矩問題的,沒有同步方法的調用怎么會有這些問題呢?

2. async,await 和 Task 什么關系

嘗試過這一種寫法:

/* 錯誤寫法 */
private static async byte AsyncCallReadLargeFile()
{
    return await AsyncCallReadLargeFile();
}

會有如下錯誤提示:

錯誤 CS1061 “byte”未包含“GetAwaiter”的定義,並且找不到可接受第一個“byte”類型參數的可訪問擴展方法“GetAwaiter”(是否缺少 using 指令或程序集引用?)

似乎可以認為,只有返回的類型包含 GetAwaiter 的定義,才能被當作異步函數來調用。最常見的只有 Task 包含這個方法。想到之前看到過,async 修飾的函數,返回類型只能是 void, Task, Task

3. 異步方法的返回

在 AsyncCallReadLargeFile 函數中,雖然簽名中返回類型是 Task<byte> ,但我們實際上只返回了 byte 類型,並沒有 Task。我的理解是對於 async 修飾的異步方法,返回的類型會自動被包裝成 Task 的泛型類型。

參考

深入理解async和await的作用及各種適用場景和用法(舊,詳見最新兩篇)

C# 徹底搞懂async/await

(這兩篇都很全面,受益匪淺)

朝夕教育 bilibili 視頻


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM