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?
下一篇我將來探討一下這個問題,感興趣的小夥伴可以關注留意後續更新
有說得不對的地方歡迎大神指正,歡迎討論,共同進步