1、異步編程
異步編程是一項關鍵技術,可以直接處理多個核心上的阻塞 I/O 和並發操作。 通過 C#、Visual Basic 和 F# 中易於使用的語言級異步編程模型,.NET 可為應用和服務提供使其變得可響應且富有彈性。
上面是關於異步編程的解釋,我們日常編程過程或多或少的會使用到異步編程,為什么要試用異步編程?因為用程序處理過程中使用文件和網絡 I/O,比如處理文件的讀取寫入磁盤,網絡請求接口API,默認情況下 I/O API 一般會阻塞。
這樣的結果是導致我們的用戶界面卡住體驗差,有些服務器的硬件利用率低,服務處理能力請求響應慢等問題。基於任務的異步 API 和語言級異步編程模型改變了這種模型,只需了解幾個新概念就可默認進行異步執行。
現在普遍使用的異步編程模式是TAP模式,也就是C# 提供的 async 和 await 關鍵詞,實際上我們還有另外兩種異步模式:基於事件的異步模式 (EAP),以及異步編程模型 (APM) 。
APM 是基於 IAsyncResult 接口提供的異步編程,例如像FileStream類的BeginRead,EndRead就是APM實現方式,提供一對開始結束方法用來啟動和接受異步結果。使用委托的BeginInvoke和EndInvoke的方式來實現異步編程。
EAP 是在 .NET Framework 2.0 中引入的,比較多的體現在WinForm編程中,WinForm編程中很多控件處理事件都是基於事件模型,經常用到跨線程更新界面的時候就會使用到BeginInvoke和Invoke。事件模式算是對APM的一種補充,定義了一系列事件包括完成、進度、取消的事件讓我們在異步調用的時候能注冊響應的事件進行操作。
class Program
{
static void Main(string[] args)
{
Console.WriteLine(DateTime.Now + " start");
IAsyncResult result = BeginAPM();
//EndAPM(result);
Console.WriteLine(DateTime.Now + " end");
Console.ReadKey();
}
delegate void DelegateAPM();
static DelegateAPM delegateAPM = new DelegateAPM(DelegateAPMFun);
public static IAsyncResult BeginAPM()
{
return delegateAPM.BeginInvoke(null, null);
}
public static void EndAPM(IAsyncResult result)
{
delegateAPM.EndInvoke(result);
}
public static void DelegateAPMFun()
{
Console.WriteLine("DelegateAPMFun...start");
Thread.Sleep(5000);
Console.WriteLine("DelegateAPMFun...end");
}
}
如上代碼我使用委托實現異步調用,BeginAPM 方法使用 BeginInvoke 開始異步調用,然后 DelegateAPMFun 異步方法里面停5秒。看下下面的打印結果,是 main 方法里面的打印在前,異步方法里面的打印在后,說明該操作是異步的。
其中一行代碼EndAPM(result)
被注釋了,調用了委托 EndInvoke 方法,該方法會阻塞程序直到異步調用完成,所以我們可以放到適當的位置用來獲取執行結果,這類似於TAP模式的await 關鍵字,放開改行代碼執行下。
以上兩種方式已不推薦使用,編寫理解起來比較晦澀,感興趣的可以自行了解下,而且這種方式在.net 5里面已經不支持委托的異步調用了,所以如果要運行需要在.net framework框架下。
TAP 是在 .NET Framework 4 中引入的,是目前推薦的異步設計模式,也是我們本文討論的重點方向,但是TAP並不一定是線程,他是一種任務,理解為工作的異步抽象,而非在線程之上的抽象。
2、async await
使用 async await 關鍵字可以很輕松的實現異步編程,我們子需要將方法加上 async 關鍵字,方法內的異步操作使用 await 等待異步操作完成后再執行后續操作。
class Program
{
static void Main(string[] args)
{
Console.WriteLine(DateTime.Now + " start");
AsyncAwaitTest();
Console.WriteLine(DateTime.Now + " end");
Console.ReadKey();
}
public static async void AsyncAwaitTest()
{
Console.WriteLine("test start");
await Task.Delay(5000);
Console.WriteLine("test end");
}
}
AsyncAwaitTest 方法使用 async 關鍵字,使用await關鍵字等待5秒后打印"test end"。在 Main 方法里面調用 AsyncAwaitTest 方法。
使用 await 在任務完成前將控制讓步於其調用方,可讓應用程序和服務執行有用工作。 任務完成后代碼無需依靠回調或事件便可繼續執行。 語言和任務 API 集成會為你完成此操作。
使用await 的方法必須使用 async 關鍵字,如果我們 Main 方法里面想等待 AsyncAwaitTest 則 Main 方法需要加上 async 並返回 Task。
3、async await 原理
將上面 Main 方法不使用 await 調用的方式編譯后使用ILSpy反編譯dll,使用C# 4.0才能看到編譯器為我們做了什么。因為4.0不支持 async await 所以會反編譯到具體代碼,4.0 以后的反編譯后會直接顯示 async await 語法。
通過反編譯后可以看到在異步方法里面重新生成了一個泛型類
stateMachine.MoveNext()
即調用
public static void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
if (stateMachine == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine);
}
Thread currentThread = Thread.CurrentThread;
Thread thread = currentThread;
ExecutionContext executionContext = currentThread._executionContext;
ExecutionContext executionContext2 = executionContext;
SynchronizationContext synchronizationContext = currentThread._synchronizationContext;
try
{
stateMachine.MoveNext();
}
finally
{
SynchronizationContext synchronizationContext2 = synchronizationContext;
Thread thread2 = thread;
if (synchronizationContext2 != thread2._synchronizationContext)
{
thread2._synchronizationContext = synchronizationContext2;
}
ExecutionContext executionContext3 = executionContext2;
ExecutionContext executionContext4 = thread2._executionContext;
if (executionContext3 != executionContext4)
{
ExecutionContext.RestoreChangedContextToThread(thread2, executionContext3, executionContext4);
}
}
}
我們再看編譯器為生成的類 <AsyncAwaitTest>d__1
:
MoveNext方法將 AsyncAwaitTest 邏輯代碼包含進去了,我們的源代碼因為只有一個 await 操作,如果有多個 await 操作,那么MoveNext里面應該還會有多個分段邏輯,將不同段的MoveNext放入不同的狀態分段塊。
在該類中也有一個if判斷,按照 1__state 狀態參數,最開始調用的時候是-1,執行進來 num != 0
則執行我們的業務代碼if里面的,這個時候會順序執行業務代碼,直到碰到 await 則執行如下代碼
awaiter = Task.Delay(5000).GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (<> 1__state = 0);
<> u__1 = awaiter;
< AsyncAwaitTest > d__1 stateMachine = this;
<> t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
return;
}
在該過程中 <> t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine)
將 await 句和狀態機進行傳遞調用 AwaitUnsafeOnCompleted
方法,該方法一直跟下去會找到線程池的操作。
// System.Threading.ThreadPool
internal static void UnsafeQueueUserWorkItemInternal(object callBack, bool preferLocal)
{
s_workQueue.Enqueue(callBack, !preferLocal);
}
程序將封裝的任務放入線程池進行調用,這個時候異步方法就切換到了另一個線程,或者在原線程上執行(如果異步方法執行時間比較短可能就不會進行線程切換,這個主要看調度程序)。
執行完成 await 后狀態 1__state 已經更改了為 0,程序會再次調用 MoveNext 進入 else 之后沒有return和其它邏輯,則繼續執行到結束。
可以看到這是一個狀態控制的執行邏輯,是一種“狀態機模式”的設計模式,對於 Main 方法調用 AsyncAwaitTest 邏輯此刻進入if,碰到await則進入線程調度執行,如果異步方法切換到其它線程調用,則方法 Main 繼續執行,當狀態機執行切換到另外一個狀態后再次 MoveNext 直到執行完異步方法。
4、async 與 線程
有了上面的基礎我們知道 async 與 await 通常是成對配合使用的,當我們的方法標記為異步的時候,里面的耗時操作就需要 await 進行標記等待完成后執行后續邏輯,調用該異步方法的調用者可以決定是否等待,如果不用 await 則調用者異步執行或者就在原線程上執行異步方法。
如果 async 關鍵字修改的方法不包含 await 表達式或語句,則該方法將同步執行,可選擇性通過 Task.Run API 顯式請求任務在獨立線程上運行。
可以將 AsyncAwaitTest 方法改為顯示線程運行:
public static async Task AsyncAwaitTest()
{
Console.WriteLine("test start");
await Task.Run(() =>
{
Thread.Sleep(5000);
});
Console.WriteLine("test end");
}
5、取消任務 CancellationToken
如果不想等待異步方法完成,可以通過 CancellationToken 取消該任務,CancellationToken 是一個struct,通常使用 CancellationTokenSource 來創建 CancellationToken,因為CancellationTokenSource 有一些列的[方法]用於我們取消任務而不用去操作CancellationToken 結構體。
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken ct = cts.Token;
然我改造下方法,將 CancellationToken 傳遞到異步方法,cts.CancelAfter(3000)
3秒鍾后取消任務,我們監聽CancellationToken 如果 IsCancellationRequested==true
則直接返回 。
static void Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken ct = cts.Token;
cts.CancelAfter(3000);
Console.WriteLine(DateTime.Now + " start");
AsyncAwaitTest(ct);
Console.WriteLine(DateTime.Now + " end");
Console.ReadKey();
}
public static async Task AsyncAwaitTest(CancellationToken ct)
{
Console.WriteLine("test start");
await Task.Delay(5000);
Console.WriteLine(DateTime.Now + " cancel");
if (ct.IsCancellationRequested) {
return;
}
//ct.ThrowIfCancellationRequested();
Console.WriteLine("test end");
}
因為我們是手動通過代碼判斷狀態結束異步,所以即使在3秒后就已經結束了任務,但是await Task.Delay(5000)
任然會等待5秒執行完。還有一種方式就是我們不判斷是否取消,直接調用ct.ThrowIfCancellationRequested()
給我們判斷,這個方法如果,但是任然不能及時結束。這個時候我們還有另外一種處理方式,就是將CancellationToken 傳遞到 await 的異步API方法里,可能會立即結束,也可能不會,這個要取決異步實現。
public static async Task AsyncAwaitTest(CancellationToken ct)
{
Console.WriteLine("test start");
//傳遞CancellationToken 取消
await Task.Delay(5000,ct);
Console.WriteLine(DateTime.Now + " cancel");
//手動處理取消
//if (ct.IsCancellationRequested) {
// return;
//}
//調用方法處理取消
//ct.ThrowIfCancellationRequested();
Console.WriteLine("test end");
}
6、注意項
在異步方法里面不要使用 Thread.Sleep 方法,有兩種可能:
1、Sleep在 await 之前,則會直接阻塞調用方線程等待Sleep。
2、Sleep在 await 之后,但是 await 執行在調用方的線程上也會阻塞調用方線程。
所以我們應該使用 Task.Delay 用於等待操作。那為什么我上面的 Task.Run 里面使用了 Thread.Sleep呢,因為 Task.Run 是顯示請求在獨立線程上運行,所以我知道這里寫不會阻塞調用方,上面我只是為了演示,所以不建議用。