您的位置:首页 > 编程语言 > ASP

ASP.NET sync over async(异步中同步,什么鬼?)

2015-07-08 13:16 603 查看
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,但仅限于不返回结果的执行线程。

当然最好的方式是异步到底
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: