如何使用 .NET Core、C# 和 VS Code 中的 async/await 提高 .NET 程序的响应速度
在Twitter上关注我,很高兴接受您对主题或改进的建议/Chris
当我们运行同步代码时,我们会阻止主线程执行除当前正在执行的操作之外的任何其他操作。这会导致软件运行速度和用户体验比预期更慢。
简而言之:.NET/.NET Core 中存在线程的概念,它们是调度并行执行工作的绝佳方式。然而,它们使用起来可能比较麻烦。不过,有一个名为 TPL(Task Parallel Library )的库,它基于线程模型,可以非常轻松地调度和管理工作。
参考
-
异步返回类型
它为您提供了任务的良好介绍。 -
任务控制流
这讨论了控制流,如何确保代码按照正确的顺序执行 -
基于任务的异步编程
这更多是基于任务的编程的概述 -
如何按顺序运行任务
这讨论如何按顺序一个接一个地运行任务。 -
任务取消
这将教你如何取消任务并监听取消消息 -
Azure 中的 Durable Functions
展示了如何在无服务器编程中执行任务,特别是 Durable Functions -
将同步代码转换为异步的配方
这将引导您从同步代码逐步将其转换为异步代码。
什么
所以我们提到 TPL 是一个库。我们需要知道什么?TPL 是一个非常核心且重要的概念,它存在于核心 API 中。它是System.Threading
和System.Threading.Tasks
命名空间的一部分。它为我们做了很多事情,例如:
- 工作划分
- 线程调度
ThreadPool
- 取消支持
- 状态管理
以及其他低级细节。
我们需要了解一些基本概念。
- Task,一个任务代表一个异步操作,例如从文件获取内容或进行需要时间的计算。Task 有一些有趣的属性,允许我们与 UI 进行通信,例如异步工作的进展情况,例如:
- 状态,这可以告诉我们它当前是否正在处理某件事、是否已完成、是否出错或是否已取消
- IsCanceled,如果取消,则设置为
true
- IsFaulted,如果出现问题(例如异常),则将其设置为
true
- IsCompleted,一旦完成操作,它将被设置为
true
- Async/Await。该
await
关键字表示我们等待异步操作结束,并在操作结束时获得结果,例如var fileContent = await GetFileAsync()
。任何使用该await
概念的方法都需要将async
关键字作为方法头的一部分。 - 阻塞/非阻塞。当我们使用 Task 时,我们不会阻塞,其他线程可以继续工作。不过,当我们
Wait()
在 Task 上使用该方法时,会有一些例外。这时,我们会强制代码同步运行。我们将在下一节的演示中展示这一点。
为什么
很多事情,例如打开大文件、执行 Web 请求,甚至搜索计算机,都可以并行完成。这意味着您可以更快地将结果返回给用户,您的应用也会被认为运行速度更快、响应更灵敏。Web 开发已经大量使用了任务的概念,这是 TPL 的核心概念。学习如何使用 TPL 可以真正提升您的应用程序的响应速度。我希望通过本文,您能够更好地掌握 TPL 和任务的使用方法。
演示
在我们的演示中,我们将演示以下内容:
- 编写方法,如何使用编写方法
async/await
以及如何返回不同类型的方法 - 控制流,我们将展示如何等待所有以及特定的任务
- 阻塞代码,我们将展示如何使用
Result
以及如何Wait()
影响您的代码
搭建项目脚手架
让我们首先创建一个如下解决方案:
mkdir tasks
cd tasks
dotnet new sln
这应该创建一个解决方案文件。
接下来,我们将创建一个控制台项目,如下所示:
dotnet new solution -o task-demo
现在将其添加到解决方案中,如下所示:
dotnet sln add task-demo/task-demo.csproj
好的,我们准备开始编码了。打开一个 IDE,我选择 VS Code。
创作方法
让我们打开文件Program.cs
并在类中添加以下方法Program
:
static async Task<int> Sum(int a, int b)
{
var result = await Task.FromResult(a + b);
return result;
}
上面发生了一些有趣的事情:
- 返回类型,
Task<int>
。这告诉我们它将是一个任务,一旦解决就会返回某种类型的东西int
。 Task.FromResult()
,这将创建一个给定值的 Task。我们给它指定要执行的计算,例如a+b
。- Async/Await,我们可以看到如何在方法内部使用
async
关键字来等待结果返回。async
为了确保编译器正常运行,后面需要跟上关键字。
很容易认为上述方法不需要异步,但想象一下这是一个需要时间的计算,那么它会更有意义。
还有一种情况,Task.FromResult
当答案立即已知时使用,也就是它的状态是,RanToCompletion
并且答案可以立即使用,所以你可以争辩说这await
是不必要的,因为它已经在Task.Result
属性中可用了。另一种实现上述操作的方法是:
static async Task<int> Sum2(int a, int ab)
{
var result = await Task.Run(() => {
// do some time-consuming work
return a + b;
})
return result;
}
await Sum2(1,2)
控制流
任务不仅仅是标记它们async
。我们可以确保等待所有或部分任务完成后再继续执行代码。我们有一些结构可以帮助我们控制此流程:
Task.WaitAll()
,这个方法接收一个任务列表。你本质上的意思是,所有任务都需要完成才能继续,这是阻塞的。你可以看到它返回了void
一个典型的用例,等待所有 Web 请求完成,因为我们希望返回一个由我们拼接所有数据组成的结果。Task.WaitAny()
,我们在这里也给出了一个任务列表,但含义不同。我们说只要任何一个任务完成就没问题。这通常是争夺端点的数据,或者搜索磁盘上的文件/文件内容。我们不关心谁先完成,只要我们得到响应就行。这也是阻塞并等待其中一个任务完成。Task.WhenAll()
,这会给你一个Task
可以互动的后台。所有任务完成后,它就会解决。Task.WhenAny()
,这会为您提供一个Task
可以交互的后台。当其中一个任务完成后,它将得到解决。
让我们创建一个控制流的演示。我们将通过在类中添加一个额外的方法来假装执行耗时的工作,如下所示:
static async Task DoSomething()
{
await Task.Delay(2000);
}
演示 - 控制流
现在我们可以在我们的方法中添加一些控制流代码,Main()
如下所示:
var start = DateTime.Now;
var taskSum = Sum(2,2);
var taskDelay = DoSomething();
Task.WaitAll(taskSum, taskDelay);
end = DateTime.Now;
Console.WriteLine("Time taken {0}",end - start);
我们的完整代码Program.cs
现在应该是这样的:
using System;
using System.Threading.Tasks;
using System.IO;
namespace task_demo
{
class Program
{
static async Task DoSomething()
{
await Task.Delay(2000);
}
static async Task<int> Sum(int a, int b)
{
var result = await Task.FromResult(a + b);
return result;
}
static void Main(string[] args)
{
var start = DateTime.Now;
var taskSum = Sum(2,2);
var taskDelay = DoSomething();
Task.WaitAll(taskSum, taskDelay);
end = DateTime.Now;
Console.WriteLine("Time taken! {0}", end-start);
}
}
}
让我们编译:
dotnet build
并运行它:
dotnet run
我们应该得到以下回应:
4
Time taken! 00:00:02.0026920
尽管从调用开始的计算Sum()
只花了几毫秒,但直到 2 秒后计算DoSomething()
完成时我们才得到任何响应。
如果我们现在将代码从 改为 ,WaitAll
我们WhenAll
得到的行为就会大不相同。代码会继续运行,并报告以下内容:
4
Time taken! 00:00:00.0235860
所以这里的教训是,如果我们想让代码在特定点等待,使用WaitAny
是一个好主意,但如果你想启动大量异步工作,那么使用When...
。
我们仍然可以使代码正常运行,WhenAll
但我们需要像这样调查状态:
var twoTasks = Task.WhenAll(taskSum, taskDelay);
if(twoTasks.IsCompleted)
{
var end = DateTime.Now;
Console.WriteLine("{0}", taskSum.Result);
}
DEMO - 等待任何
为了测试这一点,我们创建了三个模拟打开文件的新方法。这三个方法都内置了不同的延迟时间:
static async Task<string> ReadFile1()
{
await Task.Delay(3000);
return "file1";
}
static async Task<string> ReadFile2()
{
await Task.Delay(4000);
return "file2";
}
static async Task<string> ReadFile3()
{
await Task.Delay(2000);
return "file3";
}
让我们Program()
用一些代码来更新我们的方法:
var task1 = ReadFile1();
var task2 = ReadFile2();
var task3 = ReadFile3();
start = DateTime.Now;
Task.WaitAny(task1, task2, task3);
Console.WriteLine("Task1, completed: {0}", task1.IsCompleted);
Console.WriteLine("Task2, completed: {0}", task2.IsCompleted);
Console.WriteLine("Task3, completed: {0}", task3.IsCompleted);
Console.WriteLine("Task3, completed: {0}", task3.Result);
end = DateTime.Now;
Console.WriteLine("Time taken! {0}", end - start);
正如您在上面看到的,我们正在等待三个任务之一完成,其结构如下:
Task.WaitAny(task1, task2, task3);
根据我们对所调用方法的了解,ReadFile3()
应该在几秒钟后首先完成2
,但让我们通过运行程序来测试一下:
Task1, completed: False
Task2, completed: False
Task3, completed: True
Task3, completed: file3
Time taken! 00:00:02.0031370
我们可以从上面看到Task3
已经完成,其他任务尚未完成。
使用异步 API
好的,我们现在对异步有了更多的了解,并且能够在现有的 API 上利用它。让我们看看如何读取文件的内容。通常,你会创建一个如下方法:
static async string ReadTxtFile()
{
using(var sr = new StreamReader(File.Open("test.txt", FileMode.Open)))
{
return sr.ReadToEnd();
}
}
上面的代码会阻塞,在执行完成之前你无法做太多其他事情。想象一下,这是一个非常大的文件,那么它会非常明显。如果我们重写该方法以使用异步版本,我们会得到如下所示的代码:
static async Task<string> ReadTxtFile()
{
using(var sr = new StreamReader(File.Open("test.txt", FileMode.Open)))
{
return await sr.ReadToEndAsync();
}
}
这不会造成阻碍,每个人都很高兴。
阻塞代码
使用 TPL 的棘手之处之一在于了解哪些代码会调用阻塞。你很高兴你的代码现在是异步的,但突然之间你又陷入了阻塞状态。那么我们该注意什么呢?好吧,我们已经讨论过这个话题了:
WaitAll
和WaitAny
块,这里的经验法则似乎是它们返回 void 并使用Wait...这个词。但有时你希望它等待,所以要学会有意识地使用块/非块task.Result
,这也会阻塞并等待结果可用Wait()
,Task 上的这个方法将会阻塞,并导致你在这里等待,直到代码完成,例如Task.Delay(2000).Wait()
完整代码
如果您想自己探索的话,这是我正在使用的完整代码:
using System;
using System.Threading.Tasks;
using System.IO;
namespace task_demo
{
class Program
{
static async Task<string> ReadTxtFile()
{
using(var sr = new StreamReader(File.Open("test.txt", FileMode.Open)))
{
return await sr.ReadToEndAsync();
}
}
static string ReadFileSync1()
{
Task.Delay(2000).Wait();
return "content1";
}
static string ReadFileSync2()
{
Task.Delay(2000).Wait();
return "content2";
}
static string ReadFileSync3()
{
Task.Delay(2000).Wait();
return "content3";
}
static async Task DoSomething()
{
await Task.Delay(2000);
}
static async Task<int> Sum(int a, int b)
{
var result = await Task.FromResult(a + b);
return result;
}
static async Task<string> ReadFile1()
{
await Task.Delay(3000);
return "file1";
}
static async Task<string> ReadFile2()
{
await Task.Delay(4000);
return "file2";
}
static async Task<string> ReadFile3()
{
await Task.Delay(2000);
return "file3";
}
static void Main(string[] args)
{
var start = DateTime.Now;
var c1 = ReadFileSync1();
var c2 = ReadFileSync2();
var c3 = ReadFileSync3();
var end = DateTime.Now;
Console.WriteLine("Time taken {0}", end-start);
start = DateTime.Now;
var taskSum = Sum(2,2);
var taskDelay = DoSomething();
Task.WaitAll(taskSum, taskDelay);
end = DateTime.Now;
Console.WriteLine("{0}",taskSum.Result);
Console.WriteLine("Time taken! {0}", end-start);
var task1 = ReadFile1();
var task2 = ReadFile2();
var task3 = ReadFile3();
start = DateTime.Now;
Task.WaitAny(task1, task2, task3);
Console.WriteLine("Task1, completed: {0}", task1.IsCompleted);
// this forces everyone to wait for this Task1
// Console.WriteLine("Task1, completed: {0}", task1.Result);
Console.WriteLine("Task2, completed: {0}", task2.IsCompleted);
Console.WriteLine("Task3, completed: {0}", task3.IsCompleted);
Console.WriteLine("Task3, completed: {0}", task3.Result);
end = DateTime.Now;
Console.WriteLine("Time taken! {0}", end - start);
}
}
}
概括
总而言之,我们学习了任务的概念及其构成。此外,我们还学习了控制流,并讨论了阻塞/非阻塞代码。不过,还有更多内容需要学习,例如如何取消任务。我将另开一篇文章来介绍取消操作。我会在本文的参考资料部分添加“取消”部分的链接。
文章来源:https://dev.to/dotnet/how-you-can-make-your-net-programs-faster-with-asynchronous-code-in-net-core-c-and-vs-code-471c