vlambda博客
学习文章列表

.Net异步编程(3)异步编程场景以及最佳实践

前面我们学习了如何使用关键字async以及await和类Task进行异步编程,并且通过一个例子演示了如何进行异步方法的设计以及优化,今天我们继续学习一下异步编程主要应用场景,以及如何应对这些场景。

在.Net中异步应用的场景主要有两种:

  • 基于IO的异步编程:例如:从网络上请求数据,存取数据库,读写文件等等。

  • 基于CPU密集计算的异步编程,例如很多需要大量的CPU计算的应用场景。

以上这两个应用场景是.Net的异步编程模型中非常常见的应用场景,应用.Net的异步编程模型实际上是主要是基于类Task以及Task<T>的泛型模型,在我们的模型中通过关键字async以及await来应用他们,这个在我们之前的文章中也详细的描述过了。针对以上两个场景,一般推荐的做法如下:

  • 基于IO的异步应用场景直接在async定义的异步方法中使用await等待IO操作,这个和我们之前文章描述的是一致的。

  • 基于CPU计算的场景,建议在async定义的异步方法中使用await来等待一个由Task.run开始运行的一个方法,注意使用Task.run会将任务放入到后台线程中去。

之前我们的文章也强调过await的作用以及当方法调用者运行到await代码时,CLR会将已经在被调用的方法里的控制点返回给调用方法,从而可以使得调用方法可以不被block然后可以用于其他任务,例如用于处理UI的线程等等,关于异步模式的设计有很多种设计模式,后面我们使用几篇学习日志来向大家介绍异步编程的设计模式。目前仅仅需要记住在.Net中使用的是async以及await以及Task来使用基于TAP的异步编程。

实例

我们先来学习几个基于IO和基于CPU的异步编程例子。

基于IO的异步编程实例

从一个Web服务下载数据,这个实例是一个典型的基于IO的异步编程模型,我们这个实例是在UI的环境中,当按钮被按下,从web服务下载数据的任务并不会block UI的线程,可以使用如下的代码来演示:

private readonly HttpClient _httpClient = new HttpClient();

downloadButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI as the request
// from the web service is happening.
//
// The UI thread is now free to perform other work.
// 从这个await开始,控制点将会被让给UI控制线程,
var stringData = await _httpClient.GetStringAsync(URL);
DoSomethingWithData(stringData);
};

基于CPU计算的异步编程实例

我们这里使用了一个游戏的例子,在这个例子中需要计算对游戏中的敌人进行计算,可以使用如下的代码对这个场景进行模拟,是一个CPU密集型的异步编程场景。

private DamageResult CalculateDamageDone()
{
// Code omitted:
//
// Does an expensive calculation and returns
// the result of that calculation.
}

calculateButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI while CalculateDamageDone()
// performs its work. The UI thread is free to perform other work.
var damageResult = await Task.Run(() => CalculateDamageDone());
DisplayDamage(damageResult);
};

在这个例子里我们可以看到基于CPU密集型的任务计算,我们直接使用了Task.run()方法将需要执行的代码放入后台线程,并且不需要我们手动进行后台线程管理。

一些重要的概念

从纯理论来看,asyncawaitTask是典型的设计模式Promise Model of asynchrony的实现,之后我们也会学习这个模式设计。

如下一些重要的概念我们时刻牢记:

  • 异步代码既可以应用于基于IO的场景也可以应用于基于CPU密集计算的场景,但是在调用上是不一样的,请参考我们的例子。

  • 异步代码使用Task<T>或者Task作为返回值,代表后台执行的任务

  • 关键字async将一个方法转为异步方法,同时需要在方法体中使用await来等待一些调用。

  • 当遇到关键字awaitCLR会立即挂起当前执行的方法,并将控制点返回到方法的调用者,一直持续到等待的任务全部完成。

  • await关键字只能应用在async方法的内部。

如何甄别基于IO的异步还是基于CPU的异步

我们前面介绍过了如何分别针对这两种应用场景来使用异步编程的模型,那么如何甄别一个场景到底是基于IO的还是基于CPU就显得非常重要:

  • 你的代码是否要等待什么资源?例如从网络来的数据,从数据库中来的数据等等,如果是,那么就是基于IO的异步场景。

  • 您的代码是否要运行一些非常耗时的计算?如果是,那么就是基于CPU的异步场景。

如果是基于IO的场景,使用asyncawait,不要使用Task.run, 至于原因,我们后面会继续写文章进行分析。
如果是基于CPU的场景,使用asyncawait,同时使用Task.Run将工作切换到一个新的后台线程上,如果您的场景非常考虑并发和并行,那么请考虑使用TPL(Task Parallel Library), 这里我们明确的要理解异步并发, 以及并行

其他的例子

我们在举多一些例子

从网络下载数据

private readonly HttpClient _httpClient = new HttpClient();

[HttpGet, Route("DotNetCount")]
public async Task<int> GetDotNetCount()
{
// Suspends GetDotNetCount() to allow the caller (the web server)
// to accept another request, rather than blocking on this one.
var html = await _httpClient.GetStringAsync("https://dotnetfoundation.org");

return Regex.Matches(html, @"\.NET").Count;
}

等待多个Task运行结束

public async Task<User> GetUserAsync(int userId)
{
// Code omitted:
//
// Given a user Id {userId}, retrieves a User object corresponding
// to the entry in the database with {userId} as its Id.
}

public static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
var getUserTasks = new List<Task<User>>();
foreach (int userId in userIds)
{
getUserTasks.Add(GetUserAsync(userId));
}

return await Task.WhenAll(getUserTasks);
}

或者使用我们的神器LINQ

public async Task<User> GetUserAsync(int userId)
{
// Code omitted:
//
// Given a user Id {userId}, retrieves a User object corresponding
// to the entry in the database with {userId} as its Id.
}

public static async Task<User[]> GetUsersAsync(IEnumerable<int> userIds)
{
var getUserTasks = userIds.Select(id => GetUserAsync(id));
return await Task.WhenAll(getUserTasks);
}

所以LINQ的返回值也是一个Task, 使用await会自动返回需要的类型。只是这里有一个需要注意的地方,因为LINQ使用延迟加载的方式,因此这里使用了Select并不会立即执行。

最佳实践

  • async方法体中至少需要一个await,否则并不会将控制点转交给调用者。

  • 异步方法最好以Async结尾表示一个异步方法,这虽然不是强制的,但是这是一个约定,建议要遵守。

  • async void仅仅用在事件处理中,其他的场景请使用Task

  • 在结合lambda表达式和LINQ的时候谨慎使用async, 这主要是在LINQ中延迟运行特性带来的复杂性。

  • 使用await避免引发block:

    • 使用await代替Task.WaitTask.Result

    • 使用await Task.WhenAny代替Task.WhenAny

    • 使用await Task.WhenAll代替Task.WhenAll

    • 使用await Task.Delay代替Thread.sleep

  • 权衡和谨慎使用ValueTask, 后面会再写一些文章来解释这个。

  • 仔细考虑使用ConfigureAwait(false), 后面也会写相应的文章来描述这个部分。

关于异步编程的应用场景和介绍今天就到这里了。我们下一节再继续学习。


.Net异步编程指南




主题 文章列表
Azure机器学习入门系列
Azure云架构师入门修炼系列
Azure认知服务
Blazor文章列表     
ASP.net Core文章列表







AzureDeveloper,一个分享和学习Azure技术的好去处,欢迎关注