1.同步與異步
假設存在
IO事件A:請求網路資源 (完成耗時5s)
IO事件B:查詢資料庫 (完成耗時5s)
情況一:執行緒1工人在發起A請求后,一直阻塞等待,在A回應回傳結果后再接著處理事件B,那總共需要耗時>10s.
情況二:執行緒1工人在發起A請求后,馬上回傳發起B請求然后回傳,5s后事件A回應回傳,接著事件B回應回傳,那總共需要耗時<10s.
情況一就是同步的概念,而情況二就是異步的概念,細節會有所不同,但大致上可以這樣理解,然而并不是所有情況適用異步,下面將會解釋,
2.異步運行的順序
c#中的異步關鍵詞是async與await,常常結合Task使用,如下面實體,看看它執行的情況
static async Task Main(string[] args) { Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:MainStart"); //標記1 await SayHi(); Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:MainEnd"); //標記4 } static async Task SayHi() { Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:SayHiStart"); //標記2 await Task.Delay(1000); Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}:SayHiEnd"); //標記3 }
結果:
1:MainStart
1:SayHiStart
5:SayHiEnd
5:MainEnd
c#7.1后的版本都支持異步main方法,程式執行的狀況
執行緒1->標記1,
執行緒1->標記2,
執行緒5->標記3
執行緒5->標記4
執行順序如預期,而需要關注的是執行緒在執行期間的切換,在執行緒1執行完標記2后就已經回傳,接著由執行緒5接管了后面代碼邏輯的執行,那到底為什么會發生這樣的情況?
答案是:編譯器會自動地替我們完成了大量了不起的作業,下面接著來看看,
3.生成骨架與狀態機
編譯器在遇到await關鍵字會自動構建骨架與生成狀態機,按照以上例子來看看編譯器做的作業有那些,
[DebuggerStepThrough] private static void <Main>(string[] args) { Main(args).GetAwaiter().GetResult(); } [AsyncStateMachine((Type) typeof(<Main>d__0)), DebuggerStepThrough] private static Task Main(string[] args) { <Main>d__0 stateMachine = new <Main>d__0 { args = args, <>t__builder = AsyncTaskMethodBuilder.Create(), <>1__state = -1 }; stateMachine.<>t__builder.Start<<Main>d__0>(ref stateMachine); return stateMachine.<>t__builder.get_Task(); } [AsyncStateMachine((Type) typeof(<SayHi>d__1)), DebuggerStepThrough] private static Task SayHi() { <SayHi>d__1 stateMachine = new <SayHi>d__1 { <>t__builder = AsyncTaskMethodBuilder.Create(), //如果回傳的是void builder為AsyncVoidMethodBuilder <>1__state = -1 //狀態初始化為-1 }; stateMachine.<>t__builder.Start<<SayHi>d__1>(ref stateMachine); //開始執行 傳入狀態機的參考 return stateMachine.<>t__builder.get_Task(); //回傳結果 }
1.編譯器會自動生成void mian程式入口方法,它會呼叫async Task main方法,(所以說c#7.1支持異步main方法,其實只是編譯器做了一點小作業)
2.main方法里的輸出內容與呼叫SayHi方法代碼消失了,取而代之的是編譯器生成了骨架方法,初始化 <Main>d__0 狀態機,把狀態機的狀態欄位<>1__state
初始化為-1,builder為AsyncTaskMethodBuilder實體,接著呼叫builder的Start方法,
3.SayHi方法同2
接著看看AsyncTaskMethodBuilder的Start方法
[DebuggerStepThrough] public static void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine: IAsyncStateMachine { if (((TStateMachine) stateMachine) == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine); } Thread currentThread = Thread.CurrentThread; Thread thread2 = currentThread; ExecutionContext context2 = currentThread._executionContext; SynchronizationContext context3 = currentThread._synchronizationContext; try { stateMachine.MoveNext(); //呼叫了狀態機的MoveNext方法 } finally { SynchronizationContext context4 = context3; Thread thread3 = thread2; if (!ReferenceEquals(context4, thread3._synchronizationContext)) { thread3._synchronizationContext = context4; } ExecutionContext contextToRestore = context2; ExecutionContext currentContext = thread3._executionContext; if (!ReferenceEquals(contextToRestore, currentContext)) { ExecutionContext.RestoreChangedContextToThread(thread3, contextToRestore, currentContext); } } }
Start方法呼叫了狀態機的MoveNext方法,是不是很熟悉?接下來看看狀態機長什么樣子,
[CompilerGenerated] private sealed class <Main>d__0 : IAsyncStateMachine { // Fields public int <>1__state; public AsyncTaskMethodBuilder <>t__builder; public string[] args; private TaskAwaiter <>u__1; // Methods private void MoveNext() { int num = this.<>1__state; try { TaskAwaiter awaiter; if (num == 0) { awaiter = this.<>u__1; this.<>u__1 = new TaskAwaiter(); this.<>1__state = num = -1; goto TR_0004; } else //1: <>1_state初始值為-1,所以先進到該分支,由執行緒1執行 { Console.WriteLine($"{(int) Thread.get_CurrentThread().ManagedThreadId}:MainStart"); //標記1 //執行緒1執行 所以輸出 1:MainStart awaiter = Program.SayHi().GetAwaiter(); //重點:獲取Taskd GetAwaiter方法回傳TaskAwaiter if (awaiter.IsCompleted) //重點:判斷任務是否已經完成 { goto TR_0004; //SayHi方法是延時任務,所以正常情況下不會跳進這里 } else { this.<>1__state = num = 0; //賦值狀態0 this.<>u__1 = awaiter; Program.<Main>d__0 stateMachine = this; this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, Program.<Main>d__0>(ref awaiter, ref stateMachine); //重點:把TaskAwaiter與該狀態機,執行緒1執行到這回傳
}
}
return;
TR_0004:
awaiter.GetResult(); //重點:獲取結果 由執行緒1執行或延時任務不定執行緒執行
Console.WriteLine($"{(int) Thread.get_CurrentThread().ManagedThreadId}:MainEnd"); //標記4 所以輸出 5:MainEnd
this.<>1__state = -2; this.<>t__builder.SetResult();//設定結果
}
catch (Exception exception)
{
this.<>1__state = -2;
this.<>t__builder.SetException(exception); //設定例外
}
}
[DebuggerHidden] private void SetStateMachine(IAsyncStateMachine stateMachine) { } }
上面我圈了重點的是關于Task型別能實作async await的關鍵操作,
1.執行緒1執行呼叫Task實體的GetAwaiter方法回傳TaskAwaiter實體,
2.判斷TaskAwaiter實體的IsCompleted屬性是否完成,如果已完成,跳轉到TR_0004,否則執行到AwaitUnsafeOnCompleted方法,執行緒1結束回傳,
我們繼續來看看AwaitUnsafeOnCompleted方法,沒反編譯出來,所以我們來看看與它類似的AwaitOnCompleted方法( AwaitUnsafeOnCompleted實際上會呼叫UnsafeOnCompleted方法)
public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter: INotifyCompletion where TStateMachine: IAsyncStateMachine { try { awaiter.OnCompleted(this.GetStateMachineBox<TStateMachine>(ref stateMachine).MoveNextAction); } catch (Exception exception1) { Task.ThrowAsync(exception1, null); } }
看到這里是不是豁然開朗了
1.注冊TaskAwaiter實體完成任務的回呼方法,等任務完成后將會呼叫狀態機的MoveNext方法,由上篇文章Task的啟動方式知道后面的操作將會交由執行緒池的執行緒處理,所以標記3跟標記4將會在空閑的執行緒上執行,
2.<>1__state為0,跳到TR_0004執行,呼叫TaskAwaiter實體的GetResult()方法,執行await后面的代碼,回傳結果,
SayHi方法同上,
結論
編譯器遇到await后會自動構建骨架與狀態機,把await后面的代碼挪到任務完成的后面繼續執行,主執行緒第一次呼叫MoveNext方法時,如果任務已經完成會直接執行后面的操作,否則直接回傳,不阻塞主執行緒的運行,后面的流程
將交由執行緒池來調度完成,
回到文章開頭的問題,什么情況下不適用異步?
可以看出來,使用異步編譯器會生成大量額外的操作,而不耗時或者CPU密集型作業使用異步就是添堵,
思考
是不是只有Task才能用async與await?
下一篇我將來探討一下這個問題,感興趣的小伙伴可以關注留意后續更新
有說得不對的地方歡迎大神指正,歡迎討論,共同進步
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/38394.html
標籤:.NET Core
下一篇:Web API 約定
