非同步函式async await在wpf都做了什麼?
阿新 • • 發佈:2020-06-08
首先我們來看一段控制檯應用程式碼:
```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");
}
```
丟擲異常:
```
呼叫執行緒無法訪問此物件,因為另一個執行緒擁有該物件
```