目录
介绍
您对异步编程了解什么?
那么,什么是异步编程?
我们什么时候应该使用它?
任务、线程、计划、上下文
到底是怎么回事?
Asnyc编码模式
命名约定
异步任务模式
任务模式
事件模式
阻塞与死锁
推荐建议
现实世界中的一些例子
数据服务
控制器(业务层)服务
UI代码
我的知识的有用资源和来源
介绍本文提供了对DotNetCore中异步编程以及在Blazor中实现异步编程的见解。我并没有声称自己是专家:这是我最近的经验和知识积累的总结。有一些原始内容,但是我写的大部分内容都是取自其他作者的作品。底部有一个指向我发现有用的文章,博客和其他材料的链接列表,这些链接是我撰写本文时所挖掘的。富有建设性的批评和错误修复。
现代程序依赖于远程运行的数据库和服务。他们需要为延迟做好准备。了解异步编程并开发使用异步编程的应用程序正迅速成为一项关键技能。
您对异步编程了解什么?询问程序员是否了解异步编程。他们可能会点头,然后停顿一下。在开始认真使用异步编程之前,我是一个人。我很快就痛苦地意识到这种知识的深度。是的,可以肯定,我对此一无所知,可以广泛地解释它。但是实际上编写的是结构良好且行为良好的代码吗?随之而来的是谦卑的教训。
那么,什么是异步编程?简而言之,异步编程使我们可以执行多项任务,例如在与乘客交谈时驾驶汽车。Microsoft Docs网站上有一个很好的解释,描述了如何使并行任务变热或顺序执行。
我们什么时候应该使用它?与单一顺序流程相比,异步流程的三种主要情况具有明显的优势:
- 处理器密集型操作——例如复杂的数学计算
- I/0操作——将任务卸载到同一台计算机上的子系统或在远程计算机上运行
- 改善的用户界面体验
在处理器密集型操作中,您需要多个处理器或内核。将大多数处理交给这些内核,程序可以与主流程上的UI进行交互,从而更新进度并处理用户交互。在同一处理器上执行多任务一无所获。这个程序不需要更多的球来处理,只需要更多的杂耍者。
另一方面,I / O操作不需要多个处理器。他们将请求分发到子系统或远程服务,然后等待响应。现在,这是一项多任务处理,可以节省时间——一次设置并监视多个任务,然后等待它们完成。等待时间取决于运行时间最长的任务,而不是任务总数。
串行运行所有内容,并且在运行任务时锁定用户界面。异步任务可以释放UI进程。在任务运行时,UI可以与用户交互。
任务、线程、计划、上下文术语容易混淆。让我们尝试了解基本原理。
到底是怎么回事?无论代码在何处运行,本地运行还是在云中某个地方运行,一个处理器和一个内核都意味着一个人在工作。操作系统可以像一个人一样执行多任务(请注意,该任务与DotNet Core任务无关),但是只有一个人一次可以做一件事。更多并行任务,更多任务之间切换时间,更少时间进行实际工作——听起来很熟悉!使用四个处理器或四个内核,您将有四个人在工作。没有主管,一个人正在运行代码,三个人告诉他该怎么做!
现在切换回DotNet Core。操作系统多任务通过线程(NOT任务)公开。任务是打包并在线程上运行的代码块。TaskScheduler组织和安排任务。它们基本上是状态机,可跟踪执行顺序和让步以组织有效的代码执行。
所有DotNet Core应用程序都在单个线程上启动,并且所有应用程序都有一个线程池——名义上每个处理器/内核一个(尽管在云中——仅云知道)。UI和Web服务器应用程序具有一个带有同步上下文的专用应用程序线程,以运行UI或HTTP/Web上下文。这是唯一可以直接访问UI / Web上下文的线程。控制台应用程序在threadpool线程上启动并运行。这种行为上的差异具有重要意义,我们将在后面讨论。在此讨论中,AppThread等于主应用程序线程。任务是通过SynchronisationContext.Post on the 同步上下文而不是直接加载到线程上的。
UI和Web上下文应用程序在同步上下文上运行所有内容,除非另有编码。决定在哪里运行任务并将其切换到线程池是程序员的责任。一旦进入线程池,任务就由线程池管理。
TaskSchedulers在线程上运行Task。每个线程一个。执行任务时,任务创建过程首先检查TaskScheduler.Current。如果线程上当前没有正在运行的任务,则该属性为null。如果TaskScheduler存在,则有一个任务正在运行,并且新任务已加载到当前TaskScheduler任务中。如果不存在,任务创建过程将在线程上检查SynchronisationContext。如果存在,它将通过SynchronisationContext计划新任务。如果没有SynchronisationContext,则任务创建过程中得到TaskScheduler.Default、threadpool调度并加载任务进入了threadpool。
现在让我们看一下经典模式。
private async Task UIMethodAsync(...)
{
await code_Async;
dependant_code_Sync;
await code2_Async;
dependant_code2_Sync;
}
扩展语法糖,您真正拥有的是:
public Task MethodAsync(...)
{
code_Async().ContinueWith(task => {
dependant_code_Sync;
});
code2_Async().ContinueWith(task => {
dependant_code2_Sync;
});
}
扩展版本显示了实际发生的情况,并在ContinueWith代码块内包装了await代码。第二个await仅在第一个Task完成后才被调用,因此两者是无关且独立的。
如果MethodAsync直接从UI / Web上下文调用(例如响应UI事件),则任务通过加载SynchronisationContext到应用程序线程上。所有代码都在该线程上运行。
现在让我们看一个稍微不同的实现:
private async Task MethodAsync(...)
{
await Task.Run(() => code_Async);
await code2_Async;
}
第一次等待将通过Task.Run(..)创建新任务。现在,我们正在管理任务的运行位置——特别是在线程池上运行的Task.Run()任务。第二个等待块在调用方法线程(可能是应用程序线程)上运行。
为确保所有内容都在threadpool上运行,请执行以下操作:
private async EventHandler(...) => await Task.Run(() => MethodAsync(...));
private async Task MethodAsync(...)
{
await code_Async;
await code2_Async;
}
要以相同的方法并行运行多个任务,请执行以下操作:创建任务列表,然后使用WhenAll()来从任务运行它:
private async Task MethodAsync(...)
{
var tasks = new List();
tasks.Add(code_Async);
tasks.Add(code2_Async);
await Task.WhenAll(tasks);
}
Asnyc编码模式
Task对象是异步编程的核心。所有异步运行的方法都返回一个 Task:对此有一个例外,我们将在稍后讨论。可以将其Task视为代码块的包装对象。在内部,它就像一个状态机,跟踪执行顺序和收益。在外部,它提供有关代码块状态,一定级别的控制的信息,并在Task完成时公开返回值。
Async以及await旨在简化编码async方法的语法糖。
Async 将方法标记为异步并且:
- 使您可以使用await关键字等待异步任务的完成
- 完成时将任何返回值附加到任务对象
Await 挂起当前方法/代码块,并将控制权交还给它的调用者,直到任务完成。
命名约定在作为任务运行的方法名称的末尾加上Async后缀是一种常见的做法。
异步任务模式异步任务模式用于在您的方法中运行一个或多个任务的地方。您可以在同一方法中运行普通的同步代码。
经典模式:
private async Task MethodAsync(...)
{
do_normal_stuff;_
await do_some_work_Async;
do_dependant_stuff;_
}
或者:
private async Task MethodAsync(...)
{
do_normal_stuff;_
await do_some_work_Async;
do_dependant_stuff;_
return new myobject;
}
任务模式
这些模式不包含async关键字,但是可以异步运行。它们包含正常的代码,通常是相对较长的计算时间。对于async在接口和基类中声明方法也很有用。如果您标记不包含await的方法异步,则编译器会抱怨。
private Task MethodAsync(...)
{
do some work;
return Task.CompletedTask;
}
或者:
private Task MethodAsync(...)
{
do some work;
return Task.FromResult(new myobject);
}
或者:
private Task MethodAsync(...)
{
return another_task_returning_method_of_the_same_pattern();
}
事件模式
public async void MethodAsync(...);
这是例外。它返回一个没有调用机制的控制机制的void。这是一个在后台运行直到完成的激活和遗忘模式。它只能用作事件处理程序。
您将面临的最大挑战之一是死锁。始终锁定或在负载下锁定的异步代码。在Blazor中,这本身显示为锁定页面。灯亮了,但是家里没人。您已经杀死了运行SPA实例的应用程序进程。唯一的出路是重新加载页面(F5)。
正常原因是阻塞代码——在应用程序线程上的程序执行被暂停,以等待任务完成。该任务在线程的代码管道中进一步进行。暂停将阻止正在等待的代码的执行。僵局。如果将任务移至线程池,任务将完成并且块未阻止。但是,不会发生UI更新,也不会响应UI事件。将代码转移到任务池,以便您可以阻止应用程序线程不是答案。也不阻塞线程池线程。在负载下,您的应用程序可能会阻塞所有可用线程。
这是一些经典的阻止代码——在这种情况下,是UI中的按钮单击事件。
public void ButtonClicked()
{
var task = this.SomeService.GetAListAsync();
task.Wait();
}
和更多:
public void GetAListAsync()
{
var task = myDataContext.somedataset.GetListAsync();
var ret = task.Result;
}
Task.Wait()和task.Result正在阻止行动。他们停止在线程上执行,并等待任务完成。由于线程被阻塞而Task无法完成。
- 一路异步等待。不要混用同步和异步方法。从底部开始——数据或流程接口——并通过数据和业务/逻辑层一直到UI一直进行代码异步。Blazor组件同时实现异步和同步事件,因此,如果您的基本库提供了异步接口,则没有理由进行同步。
- 仅将处理器密集型任务分配给threadpool。不要因为你能就这么做。
- 不要在您的库中使用Task.Run()。将该决定尽可能保留在应用程序代码中。使您的库上下文无关。
- 切勿阻塞您的库。似乎很明显,但是...如果您绝对必须阻止,请在前端进行。
- 始终使用Async和Await,不要花哨。把事情简单化。
- 如果您的磁带库需要同时提供异步和同步调用,请分别对它们进行编码。“一次性编码”最佳实践不适用于此处。如果您不想在某个时候搬起石头砸自己的脚,切勿互相调用!
- 仅为事件处理程序使用Async Void。切勿在其他任何地方使用它。
让我们看一些现实世界中的示例,这些示例稍微有些复杂,并且获得相同结果的替代方法。
该示例站点和代码存储库是与多个Blazor文章相关联的较大项目的一部分。
该站点位于Azure上。
可以在Github的CEC.Blazor Repository上找到该代码。
显示的代码在CEC.Blazor-Async运行。
该页面获取/计算薪水。它使用数据/控制器/ UI模式进行代码隔离。数据层作为Singleton服务运行,从Entitiy Framework源提取数据。业务/控制器层作为临时服务运行。
数据服务该代码使用Task.Delay模拟查询延迟和同步FirstOrDefault以从本地列表对象获取记录,如下所示:
public async Task GetEmployeeSalaryRecord(int EmployeeID)
{
await Task.Delay(2000);
return this.EmployeeSalaryRecords.FirstOrDefault(item => item.EmployeeID == EmployeeID);
}
实时代码如下所示:
public async Task GetEmployeeSalaryRecord(int EmployeeID)
{
return await mydatacontext.GetContext().EmployeeSalaryTable.FirstOrDefaultAsync
(item => item.EmployeeID == EmployeeID);
}
请注意使用FirstorDefaultAsync进行EF调用,而不是FirstorDefault。检索列表也是如此——使用GetListAsync()。
完整的代码块如下所示:
public async Task GetEmployeeSalary(int employeeID, int egorating)
{
this.Message = "Getting Employee record";
this.MessageChanged?.Invoke(this, EventArgs.Empty);
var rec = await this.SalaryDataService.GetEmployeeSalaryRecord(employeeID);
if (rec.HasBonus)
{
this.Message = "Wow big bonus to calculate - this could take a while!";
this.MessageChanged?.Invoke(this, EventArgs.Empty);
var bonus = await Task.Run(() => this.CalculateBossesBonus(egorating));
this.Message = "Overpaid git!";
return rec.Salary + bonus;
}
else
{
this.Message = "You need a rise!";
return rec?.Salary ?? 0m;
}
}
调用数据层是:
var rec = await this.SalaryDataService.GetEmployeeSalaryRecord(employeeID);
注意await。在将结果分配给局部变量时,我们正在等待(将控制权返回给调用方法),而不是阻塞。下面的代码块看起来几乎相同,但是它的块——NONO。
var rec = this.SalaryDataService.GetEmployeeSalaryRecord(employeeID).Result;
方法的第二部分检查是否需要计算奖金。奖励计算需要占用大量处理器,因此我们将其分流到另一个线程(否则,UI可能会变慢)。我们用Task.Run来切换线程/上下文。
var bonus = await Task.Run(() => this.CalculateBossesBonus(egorating));
阻止程序任务如下所示(请注意,它不会阻止,UI代码中的调用方会阻止):
public async Task BlockerTask()
{
this.Message = "That's blown it. F5 to get out of this one.";
this.MessageChanged?.Invoke(this, EventArgs.Empty);
await Task.Delay(1000);
return true;
}
消息传递内容会更新显示消息,并告诉UI需要通过MessageChanged事件进行更新。
this.Message = "Getting Employee record";
this.MessageChanged?.Invoke(this, EventArgs.Empty);
UI代码
按钮单击与UI代码中的两个事件处理程序—— ButtonClicked和UnsafeButtonClicked连接起来。这两个事件处理程序都使用正确的异步事件处理程序模式—— async void。非阻塞代码如下所示:
public async void ButtonClicked(int employeeID)
{
.....
this.Salary = await this.SalaryControllerService.GetEmployeeSalary(employeeID, 3);
...
}
虽然阻止代码如下所示:
public async void ButtonClicked(int employeeID)
{
.....
var x = this.SalaryControllerService.BlockerTask().Result;
...
}
Index.razor代码中需要注意的其他几点:
- 有一些快速而肮脏的RenderTreeBuilder代码可以使活动感知按钮变得花哨。
- StateHasChanged()通过InvokeAsync调用。这可以确保它由Dispatcher在正确的应用程序线程上执行。
- 控制器服务的依赖注入。
- 使用Controller Service EventHandler控制组件UI的更新。
请注意,从库(EF)到UI事件的所有方法都是具有等待功能的async函数。一路异步。
我的知识的有用资源和来源- 异步编程-Microsoft
- Stephen Cleary-任务导览和其他文章
- Eke Peter-了解异步,避免C#中的死锁
- Stephen Cleary-MSDN-异步编程最佳实践