ASP.NET sync over async(异步中同步,什么鬼?)
async/await 是我们在 ASP.NET 应用程序中,写异步代码最常用的两个关键字,使用它俩,我们不需要考虑太多背后的东西,比如异步的原理等等,如果你的 ASP.NET 应用程序是异步到底的,包含数据库访问异步、网络访问异步、服务调用异步等等,那么恭喜你,你的应用程序是没问题的,但有一种情况是,你的应用程序代码比较老,是同步的,但现在你需要调用异步代码,这该怎么办呢?有人可能会说,很简单啊,不是有个 .Result 吗?但事实真的就这么简单吗?我们来探究下。
首先,放出几篇经典文章:
- async & await 的前世今生
- 异步编程 In .NET(中文资料中,写异步最棒的两篇文章)
- HttpClient.GetAsync(…) never returns when using await/async
- Don't Block on Async Code(下面测试中的第三种情况)
- Should I expose synchronous wrappers for asynchronous methods?(sync over async 透彻)
上面文章的内容,我们后面会说。光看不练假把式,所以,如果真正要体会 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. 测试总结
先梳理一下测试结果:
- 异步调用使用 .Result,同步调用使用 .Result:通过,始终一个线程。
- 异步调用使用 await,同步调用使用 Task.Run:通过,三个线程,请求开始和结束为相同线程。
- 异步调用使用 await,同步调用使用 .Result:卡死,线程阻塞。
- 异步调用使用 Task.Run,同步调用使用 .Result:卡死,线程阻塞。
- 异步调用使用 await .ConfigureAwait(true),同步调用使用 .Result:卡死,线程阻塞。
- 异步调用使用 await .ConfigureAwait(false),同步调用使用 .Result:通过,两个线程,await 执行为单独一个线程。
- 异步调用使用 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,但仅限于不返回结果的执行线程。
当然最好的方式是异步到底。
ASP.NET sync over async(异步中同步,什么鬼?)的更多相关文章
- Async 异步转同步详细流程解释
安装 npm install async --save 地址 https://github.com/caolan/async Async的内容主要分为三部分 流程控制: 简化九种常见的流程的处理 ...
- async异步改同步后怎么监听错误
当我们使用readFile()这种api,它第一个参数是报的错误,当使用async.await把它改写成同步,我们可以使用try { }catch{ }解决.
- ajax中的async属性值之同步和异步及同步和异步区别
jquery中ajax方法有个属性async用于控制同步和异步,默认是true,即ajax请求默认是异步请求,有时项目中会用到AJAX同步.这个同步的意思是当JS代码加载到当前AJAX的时候会把页面里 ...
- 【Azure 应用服务】由 Azure Functions runtime is unreachable 的错误消息推导出 ASYNC(异步)和 SYNC(同步)混用而引起ThreadPool耗尽问题
问题描述 在Azure Function Portal上显示: Azure Functions runtime is unreachable,引起的结果是Function App目前不工作,但是此前一 ...
- js中的异步与同步,解决由异步引起的问题
之前在项目中遇到过好多次因为异步引起的变量没有值,所以意识到了认识js中同步与异步机制的重要性 在单线程的js中,异步代码会被放入一个事件队列,等到所有其他代码执行后再执行,而不会阻塞线程. 下面是j ...
- 简单的node爬虫练手,循环中的异步转同步
简单的node爬虫练手,循环中的异步转同步 转载:https://blog.csdn.net/qq_24504525/article/details/77856989 看到网上一些基于node做的爬虫 ...
- IO中同步、异步与阻塞、非阻塞的区别
一.同步与异步同步/异步, 它们是消息的通知机制 1. 概念解释A. 同步所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回. 按照这个定义,其实绝大多数函数都是同步调用(例如si ...
- C#中的异步和同步
同步 同步(英语:Synchronization [ˌsɪŋkrənaɪ'zeɪʃn]),指对在一个系统中所发生的事件(event)之间进行协调,在时间上出现一致性与统一化的现象.说白了就是多个任务一 ...
- IO中同步、异步与阻塞、非阻塞的区别(转)
转自:http://blog.chinaunix.net/uid-26000296-id-3754118.html 一.同步与异步同步/异步, 它们是消息的通知机制 1. 概念解释A. 同步所谓同步, ...
随机推荐
- Servlet解决参数乱码问题
为什么会产生乱码? 之所以会产生乱码,是由于服务器端和客户端的编码方式不一致造成的.客户端与服务器端的交互过程中,存在着两次数据交换:第一次,客户端向服务器端发起请求,第二次数据交换,服务器端响应客户 ...
- 【hihoCoder】1148:2月29日
问题:http://hihocoder.com/problemset/problem/1148 给定两个日期,计算这两个日期之间有多少个2月29日(包括起始日期). 思路: 1. 将问题转换成求两个日 ...
- css 清除浮动 clear
.clearfix{ zoom:1;/*对于老版本的IE进行兼容的设置*/ } .clearfix:after{ content:""; display:block; visibi ...
- android 从服务器获取新闻数据并显示在客户端
新闻客户端案例 第一次进入新闻客户端需要请求服务器获取新闻数据,做listview的展示, 为了第二次再次打开新闻客户端时能快速显示新闻,需要将数据缓存到数据库中,下次打开可以直接去数据库中获取新闻直 ...
- 浅谈PHP7新特性
1. 运算符(NULL 合并运算符) 把这个放在第一个说是因为我觉得它很有用.用法: $a = $_GET['a'] ?? 1; 它相当于: $a = isset($_GET['a']) ? $_GE ...
- mac远程桌面连接windows 8.1 update,提示: 远程桌面连接无法验证您希望连接的计算机的身份
在网上找到解决方案: SolutionEnable RDP security layer in Group Policy on the machine: Verify that the firewal ...
- SQL Server数据库镜像的页面自动修复原理
SQL Server数据库镜像的页面自动修复原理 主库页面损坏 镜像库页面损坏 LSN用来保证事务的时序 LSN保存在每个数据页面的页头 在同一台机器,内存中的数据页和磁盘中的数据页保持同步依靠的是数 ...
- Kruskal 最小生成树算法
对于一个给定的连通的无向图 G = (V, E),希望找到一个无回路的子集 T,T 是 E 的子集,它连接了所有的顶点,且其权值之和为最小. 因为 T 无回路且连接所有的顶点,所以它必然是一棵树,称为 ...
- 浅谈Excel开发:十一 针对64位Excel的插件的开发和部署
自Office 2010版本开始有了32位和64位之分,对Excel来说,32位的Excel和64位的Excel在性能上的主要区别是64位的Excel能够处理2G及2G以上的大数据集. 随着64位操作 ...
- Python 学习文章收藏
作者 标题 rollenholt Python修饰器的函数式编程 - Rollen Holt - 博客园 rollenholt python操作gmail - Rollen Holt - 博客园 ro ...