走进异步世界async、await

此处少了一张图:不过无伤大雅!

上述代码是对真实案例的简化,我点运行之后,发现执行到HttpResponseMessage httpResp = await httpClient.PostAsJsonAsync(reqUrl, t);红色小箭头进入到这句代码之后就消失的无影无踪,我等了半宿,然后……然后就没有然后了,没有异常,只有寂寞。

关于async和await,这兄弟俩是对异步编程的语法简化。谈到异步,就涉及到线程和逻辑执行顺序,看下面代码就一清二楚了。

class Program
{
static void Main(string[] args)
{
Console.WriteLine("step1,线程ID:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId);

AsyncDemo demo = new AsyncDemo();
//demo.AsyncSleep().Wait();//Wait会阻塞当前线程直到AsyncSleep返回
demo.AsyncSleep();//不会阻塞当前线程

Console.WriteLine("step5,线程ID:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId);
Console.ReadLine();
}
}

public class AsyncDemo
{

public async Task AsyncSleep()
{
Console.WriteLine("step2,线程ID:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId);

//await关键字表示“等待”Task.Run传入的逻辑执行完毕,此时(等待时)AsyncSleep的调用方能继续往下执行(准确地说,是当前线程不会被阻塞)
//Task.Run将开辟一个新线程执行指定逻辑
await Task.Run(() => Sleep(10));

Console.WriteLine("step4,线程ID:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId);
}

private void Sleep(int second)
{
Console.WriteLine("step3,线程ID:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId);

扫描二维码关注公众号,回复: 4658420 查看本文章

Thread.Sleep(second * 1000);
}

}

运行结果:

注意step2和step4虽然在同一个方法内部,但它们的运行线程是不同的,step4与step3一样使用Task.Run开辟的新线程。注意:假如我们在Sleep里再次使用Task.Run又开辟了新线程,假设ID为10,并通过await关键词修饰,那么step4将运行在线程10。假如将第行注释互换:

demo.AsyncSleep().Wait();
Wait会阻塞当前线程直到AsyncSleep返回
2 demo.AsyncSleep();
不会阻塞当前线程
即人为控制异步逻辑同步返回,其实这和之前获取用户信息的场景是一样一样的,猜想是在执行step2或step3后再无后续输出。
运行结果:

看来“事与愿违”。那么之前的出现的问题是怎么回事呢?既然step4和step1所在线程不一样,我们能想到什么?当然是线程死锁了!
提问:再将代码改为Task.Run(() => Sleep(10)).Wait();这时候会输出什么呢,或者说step4的输出线程ID是多少?Task.Wait();和await不一样,它会阻塞当前线程(而不管内部逻辑是否开辟了新的线程)。
运行结果:

可得step4仍运行在主线程。

线程死锁
引起线程死锁的原因有很多。在ASP.NET[ MVC]的场景中,涉及到一个概念就是AspNetSynchronizationContext。AspNetSynchronizationContext出现在.NET Framework 2.0中,因为这个版本在 ASP.NET 体系结构中引入了异步页面。在 .NET Framework 2.0 之前的版本中,每个 ASP.NET 请求都需要一个线程,直到该请求完成。 这会造成线程利用率低下,因为页面逻辑通常依赖于数据库查询和 Web 服务调用,并且处理请求的线程必须等待,直到所有这些操作结束。 使用异步页面,处理请求的线程可以开始每个操作,然后返回到 ASP.NET 线程池,当操作结束时,ASP.NET 线程池的另一个线程可以完成该请求,AspNetSynchronizationContext在这个过程中扮演了异步操作周期维护员的角色(或许还发挥了其它作用)。当一个异步操作完成,需要依赖AspNetSynchronizationContext告知页面,此时AspNetSynchronizationContext将未完成的异步操作数减1,并以同步方式处理异步线程发送过来的委托(即便是以Post“异步”方法),因此假如一个页面请求有多个异步操作同时完成,每次也只能执行一个回调委托(不同委托执行的线程不知是否是同一个,however,执行线程将具有原始页面的标识和区域)。综上所述,同一个AspNetSynchronizationContext(不知道一个AspNetSynchronizationContext实例是针对单个请求还是整个应用程序)同时只能最多被一个线程使用,结合async和await的特性,回到本文开头的代码:
SynchronizationContext 综述

public ActionResult Index()
{
//线程A阻塞,等待GetUserInfo返回,当前上下文AspNetSynchronizationContext
IEnumerable<string> a = HttpHelper.GetjsonAsync<IEnumerable<string>>("http://localhost:13817/api/Values").Result;

return View();
}

public async static Task<TResult> PostJsonAsync<TData, TResult>(string reqUrl, TData t)
{
// PostAsJsonAsync在其内部开辟新线程(设为B)异步执行,注意await并不会阻塞当前线程,而是将控制权返回方法调用方,这里是Index Action
HttpResponseMessage httpResp = await httpClient.PostAsJsonAsync(reqUrl, t);

//PostAsJsonAsync返回,但下列代码仍运行在线程B。当前方法企图重入AspNetSynchronizationContext,死锁产生在这里

if (httpResp.IsSuccessStatusCode)
{
return await httpResp.Content.ReadAsAsync<TResult>();
}
else
{
return default(TResult);
}
}

后记
await关键字并不表示后续代码马上在新线程上执行,是否开辟线程取决于是否真正创建了Task(or 从Task池中取得)。运行下面代码:

class Program
{
static void Main(string[] args)
{
Console.WriteLine($"1:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
TestTransfer1();
Console.WriteLine($"8:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
Console.ReadLine();
}

static async void TestTransfer1()
{
Console.WriteLine($"2:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
await TestTransfer2();
Console.WriteLine($"7:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
}

static async Task TestTransfer2()
{
Console.WriteLine($"3:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
await Test();
Console.WriteLine($"6:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
}

static async Task Test()
{
Console.WriteLine($"4:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
await Task.Run(() => Sleep(5)); //此处之后才开辟了新线程
Console.WriteLine($"5:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
}

static void Sleep(int second)
{
Thread.Sleep(second * 1000);
}
}

运行结果:

一目了然,所以我们不需要担心多级方法调用时会创建众多线程并切换导致的性能问题。

关于是否在await后才开始真正执行异步方法,改造上面代码如下:

class Program
{
static void Main(string[] args)
{
TestTransfer1();
Console.ReadLine();
}

static async void TestTransfer1()
{
Console.WriteLine($"1:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
var task = Test();
Sleep(2);
Console.WriteLine($"4:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
await task;
}

static async Task Test()
{
Console.WriteLine($"2:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
await Task.Run(() => Sleep(1)); //此处之后才开辟了新线程
Console.WriteLine($"3:Thread.CurrentThread.ManagedThreadId-{Thread.CurrentThread.ManagedThreadId}");
}

static void Sleep(int second)
{
Thread.Sleep(second * 1000);
}
}

运行结果:

可知在获取task实例时,异步操作就开始了,而不需要等await。由于这个特性,我们可以发起多个没有顺序依赖关系的task,最后再统一await它们,提高效率,比如分页:

var task_totalcount = query.CountAsync();
query = query.OrderBy(sortfield, sortorder);
query = query.Skip(startindex).Take(takecount);
var task_getdata = query.ToListAsync();

result.TotalCount = await task_totalcount;
result.Data = await task_getdata;

return result;

猜你喜欢

转载自www.cnblogs.com/chuizilong/p/10179814.html
今日推荐