1. 程式人生 > >非同步函式async await在wpf都做了什麼?

非同步函式async await在wpf都做了什麼?

首先我們來看一段控制檯應用程式碼: ```c# class Program { static async Task Main(string[] args) { System.Console.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}"); var result = await ExampleTask(2); System.Console.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}"); System.Console.WriteLine(result); Console.WriteLine("Async Completed"); } private static async Task ExampleTask(int Second) { await Task.Delay(TimeSpan.FromSeconds(Second)); return $"It's Async Completed in {Second} seconds"; } } ``` 輸出結果 ``` Thread Id is Thread:1,Is Thread Pool:False Thread Id is Thread:4,Is Thread Pool:True It's Async Completed in 2 seconds Async Completed ``` 如果這段程式碼在WPF執行,猜猜會輸出啥? ```c# private async void Async_Click(object sender, RoutedEventArgs e) { Debug.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}"); var result= await ExampleTask(2); Debug.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}"); Debug.WriteLine(result); Debug.WriteLine("Async Completed"); } private async Task ExampleTask(int Second) { await Task.Delay(TimeSpan.FromSeconds(Second)); return $"It's Async Completed in {Second} seconds"; } ``` 輸出結果: ``` Thread Id is Thread:1,Is Thread Pool:False Thread Id is Thread:1,Is Thread Pool:False It's Async Completed in 2 seconds Async Completed ``` 這時候你肯定是想說,小朋友,你是否有很多問號????,我們接下看下去 ### 一.**SynchronizationContext**(同步上下文) 首先我們知道async await 非同步函式本質是狀態機,我們通過反編譯工具dnspy,看看反編譯的兩段程式碼是否有不同之處: 控制檯應用: ```c# internal class Program { [DebuggerStepThrough] private static Task Main(string[] args) { Program.
d__0
d__ = new Program.
d__0();
d__.args = args;
d__.<>t__builder = AsyncTaskMethodBuilder.Create();
d__.<>1__state = -1;
d__.<>t__builder.Startd__0>(ref
d__); return
d__.<>t__builder.Task; } [DebuggerStepThrough] private static Task ExampleTask(int Second) { Program.d__1 d__ = new Program.d__1(); d__.Second = Second; d__.<>t__builder = AsyncTaskMethodBuilder.Create(); d__.<>1__state = -1; d__.<>t__builder.Startd__1>(ref d__); return d__.<>t__builder.Task; } [DebuggerStepThrough] private static void
(string[] args) { Program.Main(args).GetAwaiter().GetResult(); } } ``` WPF: ```c# public class MainWindow : Window, IComponentConnector { public MainWindow() { this.InitializeComponent(); } [DebuggerStepThrough] private void Async_Click(object sender, RoutedEventArgs e) { MainWindow.d__1 d__ = new MainWindow.d__1(); d__.<>4__this = this; d__.sender = sender; d__.e = e; d__.<>t__builder = AsyncVoidMethodBuilder.Create(); d__.<>1__state = -1; d__.<>t__builder.Start(ref d__); } [DebuggerStepThrough] private Task ExampleTask(int Second) { MainWindow.d__3 d__ = new MainWindow.d__3(); d__.<>4__this = this; d__.Second = Second; d__.<>t__builder = AsyncTaskMethodBuilder.Create(); d__.<>1__state = -1; d__.<>t__builder.Startd__3>(ref d__); return d__.<>t__builder.Task; } [DebuggerNonUserCode] [GeneratedCode("PresentationBuildTasks", "4.8.1.0")] public void InitializeComponent() { bool contentLoaded = this._contentLoaded; if (!contentLoaded) { this._contentLoaded = true; Uri resourceLocater = new Uri("/WpfApp1;component/mainwindow.xaml", UriKind.Relative); Application.LoadComponent(this, resourceLocater); } } private bool _contentLoaded; } ``` 我們可以看到完全是一致的,沒有任何區別,為什麼編譯器生成的程式碼是一致的,卻會產生不一樣的結果,我們看看建立和啟動狀態機程式碼部分的實現: ```c# public static AsyncVoidMethodBuilder Create() { SynchronizationContext synchronizationContext = SynchronizationContext.Current; if (synchronizationContext != null) { synchronizationContext.OperationStarted(); } return new AsyncVoidMethodBuilder { _synchronizationContext = synchronizationContext }; } [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Start<[Nullable(0)] TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine { AsyncMethodBuilderCore.Start(ref stateMachine); } [DebuggerStepThrough] public static void Start(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); } } } ``` 在這裡總結下: - 建立狀態機的**Create**函式通過**SynchronizationContext.Current**獲取到當前同步執行上下文 - 啟動狀態機的**Start**函式之後通過**MoveNext**函式執行我們的非同步方法 - 這裡還有一個小提示,不管**async**函式裡面有沒有**await**,都會生成狀態機,只是**MoveNext**函式執行同步方法,因此沒await的情況下避免將函式標記為async,會損耗效能 同樣的這裡貌似沒能獲取到原因,但是有個很關鍵的地方,就是**Create**函式為啥要獲取當前同步執行上下文,之後我從MSDN找到關於[SynchronizationContext]( https://docs.microsoft.com/zh-cn/archive/msdn-magazine/2011/february/msdn-magazine-parallel-computing-it-s-all-about-the-synchronizationcontext) 的介紹,有興趣的朋友可以去閱讀以下,以下是各個.NET框架使用的SynchronizationContext: | SynchronizationContext | 預設 | | -------------------------------------- | --------------- | | **WindowsFormsSynchronizationContext** | WindowsForm | | **DispatcherSynchronizationContext** | WPF/Silverlight | | **AspNetSynchronizationContext** | ASP.NET | 我們貌似已經一步步接近真相了,接下來我們來看看**DispatcherSynchronizationContext** ### 二.**DispatcherSynchronizationContext** 首先來看看**DispatcherSynchronizationContext**類的比較關鍵的幾個函式實現: ```c# public DispatcherSynchronizationContext(Dispatcher dispatcher, DispatcherPriority priority) { if (dispatcher == null) { throw new ArgumentNullException("dispatcher"); } Dispatcher.ValidatePriority(priority, "priority"); _dispatcher = dispatcher; _priority = priority; SetWaitNotificationRequired(); } //同步執行 public override void Send(SendOrPostCallback d, object state) { if (BaseCompatibilityPreferences.GetInlineDispatcherSynchronizationContextSend() && _dispatcher.CheckAccess()) { _dispatcher.Invoke(DispatcherPriority.Send, d, state); } else { _dispatcher.Invoke(_priority, d, state); } } //非同步執行 public override void Post(SendOrPostCallback d, object state) { _dispatcher.BeginInvoke(_priority, d, state); } ``` 我們貌似看到了熟悉的東西了,Send函式呼叫Dispatcher的Invoke函式,Post函式呼叫Dispatcher的BeginInvoke函式,那麼是否WPF執行非同步函式之後會呼叫這裡的函式嗎?我用dnspy進行了除錯: ![](https://img2020.cnblogs.com/blog/1294271/202006/1294271-20200607232014268-649516237.png) 我通過除錯之後發現,當等待執行完整個狀態機的之後,也就是兩秒後跳轉到該Post函式,那麼,我們可以將之前的WPF那段程式碼大概可以改寫成如此: ```c# private async void Async_Click(object sender, RoutedEventArgs e) { //async生成狀態機的Create函式。獲取到UI主執行緒的同步執行上下文 DispatcherSynchronizationContext synchronizationContext = (DispatcherSynchronizationContext)SynchronizationContext.Current; //UI主執行緒執行 Debug.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}"); //開始在狀態機的MoveNext執行該非同步操作 var result= await ExampleTask(2); //等待兩秒,非同步執行完成,再在同步上下文非同步執行 synchronizationContext.Post((state) => { //模仿_dispatcher.BeginInvoke Debug.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}"); Debug.WriteLine(result); Debug.WriteLine("Async Completed"); },"Post"); } ``` 輸出結果: ``` Thread Id is Thread:1,Is Thread Pool:False Thread Id is Thread:1,Is Thread Pool:False It's Async Completed in 2 seconds Async Completed ``` 也就是asyn負責生成狀態機和執行狀態機,await將程式碼分為兩部分,一部分是非同步執行狀態機部分,一部分是非同步執行完之後,通過之前拿到的DispatcherSynchronizationContext,再去非同步執行接下來的部分。我們可以通過dnspy除錯DispatcherSynchronizationContext的 _dispatcher欄位的Thread屬性,知道Thread為UI主執行緒,而同步介面UI控制元件的時候,也就是通過Dispatcher的BeginInvoke函式去執行同步的 ### 三.Task.ConfigureAwait Task有個ConfigureAwait方法,是可以設定是否對Task的awaiter的延續任務執行原始上下文,也就是為true時,是以一開始那個UI主執行緒的DispatcherSynchronizationContext執行Post方法,而為false,則以await那個Task裡面的DispatcherSynchronizationContext執行Post方法,我們來驗證下: 我們將程式碼改為以下: ```c# private async void Async_Click(object sender, RoutedEventArgs e) { Debug.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}"); var result= await ExampleTask(2).ConfigureAwait(false); Debug.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}"); Debug.WriteLine(result); Debug.WriteLine($"Async Completed"); } ``` 輸出: ``` Thread Id is Thread:1,Is Thread Pool:False Thread Id is Thread:4,Is Thread Pool:True It's Async Completed in 2 seconds Async Completed ``` 結果和控制檯輸出的一模一樣,且通過dnspy斷點除錯依舊進入到DispatcherSynchronizationContext的Post方法,因此我們也可以證明我們上面的猜想,而且預設ConfigureAwait的引數是為true的,我們還可以將非同步結果賦值給UI介面的Text block: ```c# private async void Async_Click(object sender, RoutedEventArgs e) { Debug.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}"); var result= await ExampleTask(2).ConfigureAwait(false); Debug.WriteLine($"Thread Id is Thread:{Thread.CurrentThread.ManagedThreadId},Is Thread Pool:{Thread.CurrentThread.IsThreadPoolThread}"); this.txt.Text = result;//修改部分 Debug.WriteLine($"Async Completed"); } ``` 丟擲異常: ``` 呼叫執行緒無法訪問此物件,因為另一個執行緒擁有該物件 ```