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的作用及各種適用場景和用法(舊,詳見最新兩篇)
(這兩篇都很全面,受益匪淺)
