zoukankan      html  css  js  c++  java
  • 异步函数async await在wpf都做了什么?

    首先我们来看一段控制台应用代码:

     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<string> 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运行,猜猜会输出啥?

          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<string> 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,看看反编译的两段代码是否有不同之处:

    控制台应用:

    internal class Program
    {
        [DebuggerStepThrough]
    	private static Task Main(string[] args)
    	{
    		Program.<Main>d__0 <Main>d__ = new Program.<Main>d__0();
    		<Main>d__.args = args;
    		<Main>d__.<>t__builder = AsyncTaskMethodBuilder.Create();
    		<Main>d__.<>1__state = -1;
    		<Main>d__.<>t__builder.Start<Program.<Main>d__0>(ref <Main>d__);
    		return <Main>d__.<>t__builder.Task;
    	}
        
    	[DebuggerStepThrough]
    	private static Task<string> ExampleTask(int Second)
    	{
    		Program.<ExampleTask>d__1 <ExampleTask>d__ = new Program.<ExampleTask>d__1();
    		<ExampleTask>d__.Second = Second;
    		<ExampleTask>d__.<>t__builder = AsyncTaskMethodBuilder<string>.Create();
    		<ExampleTask>d__.<>1__state = -1;
    		<ExampleTask>d__.<>t__builder.Start<Program.<ExampleTask>d__1>(ref <ExampleTask>d__);
    		return <ExampleTask>d__.<>t__builder.Task;
    	}
    
    	[DebuggerStepThrough]
    	private static void <Main>(string[] args)
    	{
    	        Program.Main(args).GetAwaiter().GetResult();
    	}
    }
    

    WPF:

    public class MainWindow : Window, IComponentConnector
    {
    
    	public MainWindow()
    	{
    	       this.InitializeComponent();
    	}
    
    	[DebuggerStepThrough]
    	private void Async_Click(object sender, RoutedEventArgs e)
    	{
    		MainWindow.<Async_Click>d__1 <Async_Click>d__ = new MainWindow.<Async_Click>d__1();
    		<Async_Click>d__.<>4__this = this;
    		<Async_Click>d__.sender = sender;
    		<Async_Click>d__.e = e;
    		<Async_Click>d__.<>t__builder = AsyncVoidMethodBuilder.Create();
    		<Async_Click>d__.<>1__state = -1;
    		<Async_Click>d__.<>t__builder.Start<MainWindow.<Async_Click>d__1>(ref <Async_Click>d__);
    	}
    
    	[DebuggerStepThrough]
    	private Task<string> ExampleTask(int Second)
    	{
    	        MainWindow.<ExampleTask>d__3 <ExampleTask>d__ = new MainWindow.<ExampleTask>d__3();
    		<ExampleTask>d__.<>4__this = this;
    		<ExampleTask>d__.Second = Second;
    		<ExampleTask>d__.<>t__builder = AsyncTaskMethodBuilder<string>.Create();
    		<ExampleTask>d__.<>1__state = -1;
    		<ExampleTask>d__.<>t__builder.Start<MainWindow.<ExampleTask>d__3>(ref <ExampleTask>d__);
    		return <ExampleTask>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;
    }
    

    我们可以看到完全是一致的,没有任何区别,为什么编译器生成的代码是一致的,却会产生不一样的结果,我们看看创建和启动状态机代码部分的实现:

    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<TStateMachine>(ref stateMachine);
    }
    
    [DebuggerStepThrough]
    public static void Start<TStateMachine>(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
    的介绍,有兴趣的朋友可以去阅读以下,以下是各个.NET框架使用的SynchronizationContext:

    SynchronizationContext 默认
    WindowsFormsSynchronizationContext WindowsForm
    DispatcherSynchronizationContext WPF/Silverlight
    AspNetSynchronizationContext ASP.NET

    我们貌似已经一步步接近真相了,接下来我们来看看DispatcherSynchronizationContext

    二.DispatcherSynchronizationContext

    首先来看看DispatcherSynchronizationContext类的比较关键的几个函数实现:

    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进行了调试:

    我通过调试之后发现,当等待执行完整个状态机的之后,也就是两秒后跳转到该Post函数,那么,我们可以将之前的WPF那段代码大概可以改写成如此:

    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方法,我们来验证下:

    我们将代码改为以下:

    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:

    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");
    }
    

    抛出异常:

    调用线程无法访问此对象,因为另一个线程拥有该对象
    

    补充
    推荐林大佬的一篇文章,也讲的也简洁透彻C# dotnet 自己实现一个线程同步上下文

  • 相关阅读:
    mybatis 错误 Invalid bound statement (not found)
    Redis is configured to save RDB snapshots, but is currently not able to persist on disk. Commands that may modify the data set are disabled. Please check Redis logs for details about the error.
    bug 记录 Unable to start ServletWebServerApplicationContext due to multiple ServletWebServerFactory beans
    解决:The Tomcat connector configured to listen on port 8182 failed to start. The port may already be in use or the connector may be misconfigured.
    jquery validate 验证插件 解决多个相同的Name 只验证第一个的方案
    phpStorm+xdebug调试(php7.3)
    小程序视频多个视频播放与暂停
    CSS实现单行、多行文本溢出显示省略号(…)
    Packet for query is too large (4,544,730 > 4,194,304). You can change this value on the server by setting the 'max_allowed_packet' variable.
    idea自动在文件头中添加作者和创建时间
  • 原文地址:https://www.cnblogs.com/ryzen/p/13062963.html
Copyright © 2011-2022 走看看