zoukankan      html  css  js  c++  java
  • ASP.NET sync over async(异步中同步,什么鬼?)

    async/await 是我们在 ASP.NET 应用程序中,写异步代码最常用的两个关键字,使用它俩,我们不需要考虑太多背后的东西,比如异步的原理等等,如果你的 ASP.NET 应用程序是异步到底的,包含数据库访问异步、网络访问异步、服务调用异步等等,那么恭喜你,你的应用程序是没问题的,但有一种情况是,你的应用程序代码比较老,是同步的,但现在你需要调用异步代码,这该怎么办呢?有人可能会说,很简单啊,不是有个 .Result 吗?但事实真的就这么简单吗?我们来探究下。

    首先,放出几篇经典文章:

    上面文章的内容,我们后面会说。光看不练假把式,所以,如果真正要体会 sync over async,我们还需要自己动手进行测试:

    • 1. 异步调用使用 .Result,同步调用使用 .Result
    • 2. 异步调用使用 await,同步调用使用 Task.Run
    • 3. 异步调用使用 await,同步调用使用 .Result
    • 4. 异步调用使用 Task.Run,同步调用使用 .Result
    • 5. 异步调用使用 await .ConfigureAwait(true),同步调用使用 .Result
    • 6. 异步调用使用 await .ConfigureAwait(false),同步调用使用 .Result
    • 7. 异步调用使用 await,异步调用使用 await
    • 8. 测试总结

    先说明一下,在测试代码中,异步调用使用的是 HttpClient.GetAsync 方法,并且测试请求执行两次,关于具体的分析,后面再进行说明。

    1. 异步调用使用 .Result,同步调用使用 .Result

    测试代码:

    [Route("")]
    [HttpGet]
    public string Index()
    {
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId1:" + Thread.CurrentThread.ManagedThreadId);
        var result = Test();
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId4:" + Thread.CurrentThread.ManagedThreadId);
        return result;
    }
    
    public static string Test()
    {
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId2:" + Thread.CurrentThread.ManagedThreadId);
        using (var client = new HttpClient())
        {
            var response = client.GetAsync(url).Result;
            System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId3:" + Thread.CurrentThread.ManagedThreadId);
            return response.Content.ReadAsStringAsync().Result;
        }
    }
    

    输出结果:

    Thread.CurrentThread.ManagedThreadId1:13
    Thread.CurrentThread.ManagedThreadId2:13
    Thread.CurrentThread.ManagedThreadId3:13
    Thread.CurrentThread.ManagedThreadId4:13
    Thread.CurrentThread.ManagedThreadId1:6
    Thread.CurrentThread.ManagedThreadId2:6
    Thread.CurrentThread.ManagedThreadId3:6
    Thread.CurrentThread.ManagedThreadId4:6
    

    简单总结:同步代码中调用异步,上面的测试代码应该是我们最常写的,为什么没有出现线程阻塞,页面卡死的情况呢?而且代码中调用了 GetAsync,为什么请求线程只有一个?后面再说,我们接着测试。

    2. 异步调用使用 await,同步调用使用 Task.Run

    测试代码:

    [Route("")]
    [HttpGet]
    public string Index()
    {
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId1:" + Thread.CurrentThread.ManagedThreadId);
        var result = Task.Run(() => Test2()).Result;
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId4:" + Thread.CurrentThread.ManagedThreadId);
        return result;
    }
    
    public static async Task<string> Test2()
    {
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId2:" + Thread.CurrentThread.ManagedThreadId);
        using (var client = new HttpClient())
        {
            var response = await client.GetAsync(url);
            System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId3:" + Thread.CurrentThread.ManagedThreadId);
            return await response.Content.ReadAsStringAsync();
        }
    }
    

    输出结果:

    Thread.CurrentThread.ManagedThreadId1:6
    Thread.CurrentThread.ManagedThreadId2:7
    Thread.CurrentThread.ManagedThreadId3:11
    Thread.CurrentThread.ManagedThreadId4:6
    Thread.CurrentThread.ManagedThreadId1:6
    Thread.CurrentThread.ManagedThreadId2:7
    Thread.CurrentThread.ManagedThreadId3:12
    Thread.CurrentThread.ManagedThreadId4:6
    

    简单总结:根据上面的输出结果,我们发现,在一个请求过程中,总共会出现三个线程,一个是开始的请求线程,接着是 Task.Run 创建的一个线程,然后是异步方法中 await 等待的执行线程,需要注意的是,ManagedThreadId1 和 ManagedThreadId4 始终是一样的。

    3. 异步调用使用 await,同步调用使用 .Result

    测试代码:

    [Route("")]
    [HttpGet]
    public string Index()
    {
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId1:" + Thread.CurrentThread.ManagedThreadId);
        var result = Test3().Result;
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId4:" + Thread.CurrentThread.ManagedThreadId);
        return result;
    }
    
    public static async Task<string> Test3()
    {
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId2:" + Thread.CurrentThread.ManagedThreadId);
        using (var client = new HttpClient())
        {
            var response = await client.GetAsync(url);
            System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId3:" + Thread.CurrentThread.ManagedThreadId);
            return await response.Content.ReadAsStringAsync();
        }
    }
    

    输出结果:

    Thread.CurrentThread.ManagedThreadId1:5
    Thread.CurrentThread.ManagedThreadId2:5
    

    简单总结:首先,页面是卡死状态,ManagedThreadId3 并没有输出,也就是执行到 await client.GetAsync 的时候,线程就阻塞了。

    4. 异步调用使用 Task.Run,同步调用使用 .Result

    测试代码:

    [Route("")]
    [HttpGet]
    public string Index()
    {
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId1:" + Thread.CurrentThread.ManagedThreadId);
        var result = Test4().Result;
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId4:" + Thread.CurrentThread.ManagedThreadId);
        return result;
    }
    
    public static async Task<string> Test4()
    {
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId2:" + Thread.CurrentThread.ManagedThreadId);
        return await Task.Run(() =>
        {
            Thread.Sleep(1000);
            System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId3:" + Thread.CurrentThread.ManagedThreadId);
            return "xishuai";
        });
    }
    

    输出结果:

    Thread.CurrentThread.ManagedThreadId1:6
    Thread.CurrentThread.ManagedThreadId2:6
    Thread.CurrentThread.ManagedThreadId3:7
    

    简单总结:和第三种情况一样,页面也是卡死状态,但不同的是,ManagedThreadId3 是输出的,测试它的主要目的是和第三种情况形成对比,以便了解 HttpClient.GetAsync 中到底是什么鬼?

    5. 异步调用使用 await .ConfigureAwait(true),同步调用使用 .Result

    测试代码:

    [Route("")]
    [HttpGet]
    public string Index()
    {
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId1:" + Thread.CurrentThread.ManagedThreadId);
        var result = Test5().Result;
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId4:" + Thread.CurrentThread.ManagedThreadId);
        return result;
    }
    
    public static async Task<string> Test5()
    {
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId2:" + Thread.CurrentThread.ManagedThreadId);
        using (var client = new HttpClient())
        {
            var task = client.GetAsync(url);
            var response = await task.ConfigureAwait(true);
            System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId3:" + Thread.CurrentThread.ManagedThreadId);
            return await response.Content.ReadAsStringAsync();
        }
    }
    

    输出结果:

    Thread.CurrentThread.ManagedThreadId1:6
    Thread.CurrentThread.ManagedThreadId2:6
    

    简单总结:和上面两种情况一样,页面也是卡死状态,它的效果和第三种完全一样,ManagedThreadId3 都没有输出的。

    6. 异步调用使用 await .ConfigureAwait(false),同步调用使用 .Result

    测试代码:

    [Route("")]
    [HttpGet]
    public string Index()
    {
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId1:" + Thread.CurrentThread.ManagedThreadId);
        var result = Test6().Result;
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId4:" + Thread.CurrentThread.ManagedThreadId);
        return result;
    }
    
    public static async Task<string> Test6()
    {
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId2:" + Thread.CurrentThread.ManagedThreadId);
        using (var client = new HttpClient())
        {
            var task = client.GetAsync(url);
            var response = await task.ConfigureAwait(false);
            System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId3:" + Thread.CurrentThread.ManagedThreadId);
            return await response.Content.ReadAsStringAsync();
        }
    }
    

    输出结果:

    Thread.CurrentThread.ManagedThreadId1:6
    Thread.CurrentThread.ManagedThreadId2:6
    Thread.CurrentThread.ManagedThreadId3:10
    Thread.CurrentThread.ManagedThreadId4:6
    Thread.CurrentThread.ManagedThreadId1:8
    Thread.CurrentThread.ManagedThreadId2:8
    Thread.CurrentThread.ManagedThreadId3:11
    Thread.CurrentThread.ManagedThreadId4:8
    

    简单总结:和第五种情况形成对比,仅仅只是把 ConfigureAwait 参数设置为 false,结果却完全不同。

    7. 异步调用使用 await,异步调用使用 await

    测试代码:

    [Route("")]
    [HttpGet]
    public async Task<string> Index()
    {
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId1:" + Thread.CurrentThread.ManagedThreadId);
        var result = await Test7();
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId4:" + Thread.CurrentThread.ManagedThreadId);
        return result;
    }
    
    public static async Task<string> Test7()
    {
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId2:" + Thread.CurrentThread.ManagedThreadId);
        using (var client = new HttpClient())
        {
            var response = await client.GetAsync(url);
            System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId3:" + Thread.CurrentThread.ManagedThreadId);
            return await response.Content.ReadAsStringAsync();
        }
    }
    

    输出结果:

    Thread.CurrentThread.ManagedThreadId1:6
    Thread.CurrentThread.ManagedThreadId2:6
    Thread.CurrentThread.ManagedThreadId3:12
    Thread.CurrentThread.ManagedThreadId4:12
    Thread.CurrentThread.ManagedThreadId1:7
    Thread.CurrentThread.ManagedThreadId2:7
    Thread.CurrentThread.ManagedThreadId3:8
    Thread.CurrentThread.ManagedThreadId4:8
    

    简单总结:注意这是异步的写法,调用和被调用方法都是异步的,从输出的结果中,我们就会发现,这种情况和上面的六种情况,有一个最明显的区别就是,请求线程和结束线程不是同一个,说明什么呢?线程是异步等待的。

    8. 测试总结

    先梳理一下测试结果:

    1. 异步调用使用 .Result,同步调用使用 .Result:通过,始终一个线程。
    2. 异步调用使用 await,同步调用使用 Task.Run:通过,三个线程,请求开始和结束为相同线程。
    3. 异步调用使用 await,同步调用使用 .Result:卡死,线程阻塞。
    4. 异步调用使用 Task.Run,同步调用使用 .Result:卡死,线程阻塞。
    5. 异步调用使用 await .ConfigureAwait(true),同步调用使用 .Result:卡死,线程阻塞。
    6. 异步调用使用 await .ConfigureAwait(false),同步调用使用 .Result:通过,两个线程,await 执行为单独一个线程。
    7. 异步调用使用 await,异步调用使用 await:通过,两个线程,请求开始和结束为不同线程。

    上面这么多的测试情况,看起来可能有些晕,我们先从最简单的第二种情况开始分析下,首先,页面是同步方法,请求线程可以看作是一个主线程 1,然后通过 Task.Run 创建线程 2,让它去执行 Test2 方法,需要注意的是,这时候主线程 1 并不会往下执行(从输出结果可以看出),它会等待线程 2 执行,主要是等待线程 2 执行返回结果,在 Test2 方法中,一切是异步方法,await client.GetAsync 会创建又一个线程 3 去执行,并且线程 2 等待它返回结果,然后最终回到线程 1 上,在整个过程中,虽然有三个线程,但这三个线程并不是同时工作的,而是一个执行之后等待另一个执行的结果,所以整个执行过程还是同步的。

    第三种和第二种情况的不同就是,异步调用由 Task.Run 改成了 .Result,然后就造成了页面卡死,在 Don't Block on Async Code 这篇文章中,就是详细说明的这种情况,为什么会卡死呢?其实你从同样卡死的第四种情况和第五种情况中,可以发现一些线索,ConfigureAwait 的说明是:试图继续回夺取的原始上下文,则为 true;否则为 false。什么意思呢?就是它可以变身为请求线程,最能体现出这一点的是,如果设置为 true,那么在这个线程中,就可以访问 HttpContext.Current,那为什么在同步调用中,设置为 true 就造成页面卡死呢?我们分析一下,页面是同步方法,请求线程可以看作是一个主线程 1,然后调用 Test3 异步方法,这时候主线程 1,会在这里等待异步的执行结果,在 Test3 方法中创建一个线程 2,因为把 ConfigureAwait 设置为了 true,那么线程 2 就想把自己变身成为请求线程(谋权篡位),也就是线程 1,但是人家线程 1 现在正在门口等它呢?线程 2 却想占有线程 1 的地位,很显然,这是不成功的,那什么情况下可以谋权篡位成功呢?就是线程 1 不在,也就是线程 1 回到线程池中了,这就是异步等待的效果,也是它的威力。

    针对第三种情况,简单画了一个示意图:

    在第五种情况中,因为把 ConfigureAwait 设置为 false,线程 2 不想谋权篡位了,它只想老老实实的做事,把执行结果返回给请求线程 1,那么整个请求执行过程就是顺利的。

    同步调用异步测试中,还剩一个第一种情况,它和其他情况不同的是,没有异步方法,只是使用的是 .Result,那为什么它是通过的?并且线程始终是一个呢?首先,页面请求开始,创建一个请求线程 1,因为 Test 方法并不是异步方法,所以还是线程 1 去执行它,执行到了 client.GetAsync 这一步,因为没有使用 await,所以并不会创建一个线程去执行它,并且最终的是,虽然 GetAsync 是异步方法,但再其实现代码中,设置了 ConfigureAwait(false):

    async Task<HttpResponseMessage> SendAsyncWorker(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
    {
        using (var lcts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken))
        {
            lcts.CancelAfter(timeout);
    
            var task = base.SendAsync(request, lcts.Token);
            if (task == null)
                throw new InvalidOperationException("Handler failed to return a value");
    
            var response = await task.ConfigureAwait(false);//重点
            if (response == null)
                throw new InvalidOperationException("Handler failed to return a response");
    
            //
            // Read the content when default HttpCompletionOption.ResponseContentRead is set
            //
            if (response.Content != null && (completionOption & HttpCompletionOption.ResponseHeadersRead) == 0)
            {
                await response.Content.LoadIntoBufferAsync(MaxResponseContentBufferSize).ConfigureAwait(false);
            }
    
            return response;
        }
    }
    

    所以,整个过程应该是这样的,在测试代码中始终是一个请求线程在执行,并且在 client.GetAsync 的执行中,会创建另外一个线程 2 去执行,然后线程 1 等待线程 2 的执行结果,因为 GetAsync 的实现并不在测试代码中,所以表现出来就是一个线程在执行,虽然是异步方法,但它和同步方法一样,为什么?因为线程始终在等待另一个线程的执行结果,也就是说,在某一时刻,始终是一个线程在执行,其余线程都在等待。

    sync over async(异步中同步)是否可行?通过上面的测试结果可以得出是可行的,但要注意一些写法问题:

    • 异步调用使用 .Result,而不能出现 await。
    • 不能出现 ConfigureAwait(true)。
    • 可以使用 Task.Run,但仅限于不返回结果的执行线程。

    当然最好的方式是异步到底

  • 相关阅读:
    算法:基于分布的排序算法
    剑指offer:镜像二叉树
    算法:基于比较的排序算法
    LeetCode做题笔记-135
    初识YOLO
    PHP课设图览
    浅谈C语言整型与浮点型转换
    SQL Server EXPRESS 安装
    2020CCPC 东北四省(区域)赛题目一览
    2020CCPC 黑龙江省赛题目一览
  • 原文地址:https://www.cnblogs.com/xishuai/p/asp-net-sync-over-async.html
Copyright © 2011-2022 走看看