1.同步與非同步 假設存在 IO事件A:請求網路資源 (完成耗時5s) IO事件B:查詢資料庫 (完成耗時5s) 情況一:線程1工人在發起A請求後,一直阻塞等待,在A響應返回結果後再接著處理事件B,那總共需要耗時>10s. 情況二:線程1工人在發起A請求後,馬上返回發起B請求然後返回,5s後事件A響應 ...
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?
下一篇我將來探討一下這個問題,感興趣的小伙伴可以關註留意後續更新
有說得不對的地方歡迎大神指正,歡迎討論,共同進步