目录
介绍
本文采用一种实用的方法来演示其中的一些关键概念,并介绍更复杂的编码模式。本文基于DotNetCore控制台应用程序。
您将需要一个与DotNetCore兼容的开发环境,通常是Visual Studio或Visual Code,以及与此项目相关联的Repo的副本才能运行该代码。
免责声明——该代码是实验性的,而不是生产性的。设计简洁,错误捕获和处理很少,以使其易于阅读和理解。出于相同的原因,类保持简单。
代码库
此处的代码可在GitHub Repo中找到。该项目的代码在Async-Demo中。忽略任何其他项目——它们是有关异步编程的进一步文章。
类库
在开始之前,您需要了解两个帮助程序类:
- LongRunningTasks ——模拟工作
- RunLongProcessorTaskAsync和RunLongProcessorTask使用素数计算来模拟处理器繁重的任务。
- RunYieldingLongProcessorTaskAsync 是每100次计算产生一次的版本。
- RunLongIOTaskAsync使用Task.Delay模拟缓慢的I / O操作。
- UILogger提供用于将信息记录到UI的抽象层。您将委托传递Action给方法。UILogger生成消息,然后调用Action将消息实际写入到Action配置为要写入的位置。在我们的例子LogToConsole的Program中,运行Console.WriteLine。它可以轻松地写入文本文件。
入门
我们的第一个挑战是从同步切换到异步。
确保您运行的是正确的框架和最新的语言版本。(C#7.1及更高版本支持基于Main的Task )。
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5</TargetFramework>
<LangVersion>latest</LangVersion>
<RootNamespace>Async_Demo</RootNamespace>
</PropertyGroup>
在#7.1之前的版本中Main只能同步运行,你需要一个“NONO”,使用Wait来防止Main从底部退出并关闭程序。发布#7.1后,声明Main返回Task。
async Main模式如下所示。声明async取决于代码中是否包含await:
// With await
static async Task Main(string[] args)
{
// code
// await somewhere in here
}
// No awaits
static Task Main(string[] args)
{
// code
// no awaits
return Task.CompletedTask;
}
注意事项:
- 如果使用async关键字但不带await,则编译器会发出警告,但无论如何都会进行编译,并将该方法视为同步代码。
- 您不能将方法声明为async并返回Task。您只需返回正确的值,编译器便会完成所有的繁琐工作。
因此,让我们运行一些代码。我们的第一次运行:
static Task Main(string[] args)
{
var watch = new Stopwatch();
watch.Start();
UILogger.LogThreadType(LogToConsole, "Main");
var millisecs = LongRunningTasks.RunLongProcessorTask(5);
watch.Stop();
UILogger.LogToUI(LogToConsole, $"Main ==> Completed in
{ watch.ElapsedMilliseconds} milliseconds", "Main");
return Task.CompletedTask;
}
该任务按预期同步运行。Task中的一堆同步代码。没有yield。
[11:35:32][Main Thread][Main] > running on Application Thread
[11:35:32][Main Thread][LongRunningTasks] > ProcessorTask started
[11:35:36][Main Thread][LongRunningTasks] > ProcessorTask completed in 3399 millisecs
[11:35:36][Main Thread][Main] > Main ==> Completed in 3523 milliseconds
Press any key to close this window . . .
我们的第二次运行:
static async Task Main(string[] args)
{
var watch = new Stopwatch();
watch.Start();
UILogger.LogThreadType(LogToConsole, "Main");
var millisecs = await LongRunningTasks.RunLongProcessorTaskAsync(5, LogToConsole);
UILogger.LogToUI(LogToConsole, $"Yielded to Main", "Main");
watch.Stop();
UILogger.LogToUI(LogToConsole, $"Main ==> Completed in
{ watch.ElapsedMilliseconds} milliseconds", "Main");
}
任务同步运行——无yield。逻辑上是因为没有理由yield。RunLongProcessorTaskAsync是任务中的同步代码块——计算质数——因此运行完毕。该await是多余的,它可能是一个Task,但它并没有yield,所以从未放弃线程,直到完成。
[11:42:43][Main Thread][Main] > running on Application Thread
[11:42:43][Main Thread][LongRunningTasks] > ProcessorTask started
[11:42:46][Main Thread][LongRunningTasks] > ProcessorTask completed in 3434 millisecs
[11:42:46][Main Thread][Main] > Yielded
[11:42:46][Main Thread][Main] > Main ==> Completed in 3593 milliseconds
我们的第三次运行:
static async Task Main(string[] args)
{
var watch = new Stopwatch();
watch.Start();
UILogger.LogThreadType(LogToConsole, "Main");
var millisecs = LongRunningTasks.RunYieldingLongProcessorTaskAsync(5, LogToConsole);
UILogger.LogToUI(LogToConsole, $"Yielded to Main", "Main");
watch.Stop();
UILogger.LogToUI(LogToConsole, $"Main ==> Completed in
{ watch.ElapsedMilliseconds} milliseconds", "Main");
}
在看结果之前,让我们看一下RunLongProcessorTaskAsync和RunYieldingLongProcessorTaskAsync之间的区别。我们添加了Task.Yield()来控制每100个素数。
if (isPrime)
{
counter++;
// only present in Yielding version
if (counter > 100)
{
await Task.Yield();
counter = 0;
}
}
长期运行的任务没有完成。在计算出最初的100个素数后,RunYieldingLongProcessorTaskAsync在计算完前100个素数(略低于173毫秒)后返回到Main,并且Main在yield期间运行完毕。
[12:13:56][Main Thread][Main] > running on Application Thread
[12:13:56][Main Thread][LongRunningTasks] > ProcessorTask started
[12:13:57][Main Thread][Main] > Yielded to Main
[12:13:57][Main Thread][Main] > Main ==> Completed in 173 milliseconds
如果我们更新Main到await长处理器任务:
var millisecs = await LongRunningTasks.RunYieldingLongProcessorTaskAsync(5, LogToConsole);
它运行到完成。尽管它yield了,但在继续进入Main之前,我们需要await RunYieldingLongProcessorTaskAsync Task完成。这里还有一个重要的注意事项。查看长时间运行的任务在哪个线程上运行,并将其与以前的运行进行比较。从[Main Thread]开始后,它跳到了一个新线程[LongRunningTasks Thread]。
[12:45:10][Main Thread:1][Main] > running on Application Thread
[12:45:11][Main Thread:1][LongRunningTasks] > ProcessorTask started
[12:45:14][LongRunningTasks Thread:7][LongRunningTasks] >
ProcessorTask completed in 3892 millisecs
[12:45:14][LongRunningTasks Thread:7][Main] > Yielded to Main
[12:45:14][LongRunningTasks Thread:7][Main] > Main ==> Completed in 4037 milliseconds
在RunYieldingLongProcessorTaskAsync中添加一个快速Console.Write,以查看每个生成的迭代运行在哪个线程上——编写ManagedThreadId
counter++;
if (counter > 100)
{
Console.WriteLine($"Thread ID:{Thread.CurrentThread.ManagedThreadId}");
await Task.Yield();
counter = 0;
}
结果如下所示。注意常规线程跳跃。Yield创建一个新的continuation Task,并将其调度为异步运行。首先Task.Yield,应用程序线程调度程序将新的Task消息传递给应用程序池,然后在应用程序池上,调度程序决定在何处运行Task。
引用Microsoft的Task.Yield(),”它创建了一个等待的任务,等待时异步地返回当前上下文”。我的意思是说,它是句法糖,用于对树进行控制并创建延续`Task`,并在计划它时将其发布回Scheduler以运行。进一步引用“在等待时将在等待时异步转换回当前上下文的上下文”。换句话说,除非您告知,否则它不会“等待”。在继续中访问第一个yield,然后继续执行下面的代码`Task.Yield()`。我已经测试过了
但是,以下警告适用——再次引用官方文档:
但是,上下文将决定如何相对于其他可能待处理的工作来确定该工作的优先级。在大多数UI环境中,UI线程上存在的同步上下文通常会将发布到上下文的工作的优先级高于输入和呈现工作。因此,请勿依赖于等待Task.Yield来保持UI响应。
[12:38:16][Main Thread:1][Main] > running on Application Thread
[12:38:16][Main Thread:1][LongRunningTasks] > ProcessorTask started
Thread ID:1
Thread ID:4
Thread ID:4
Thread ID:6
Thread ID:6
Thread ID:7
最后,切换到RunLongIOTaskAsync长期运行的任务。
var millisecs = await LongRunningTasks.RunLongIOTaskAsync(5, LogToConsole);
如果不await,则与以前相同:
[14:26:46][Main Thread:1][Main] > running on Application Thread
[14:26:47][Main Thread:1][LongRunningTasks] > IOTask started
[14:26:47][Main Thread:1][Main] > Yielded to Main
[14:26:47][Main Thread:1][Main] > Main ==> Completed in 322 milliseconds
如果await运行完成,请再次使用线程开关。
[14:27:16][Main Thread:1][Main] > running on Application Thread
[14:27:16][Main Thread:1][LongRunningTasks] > IOTask started
[14:27:21][LongRunningTasks Thread:4][LongRunningTasks] > IOTask completed in 5092 millisecs
[14:27:21][LongRunningTasks Thread:4][Main] > Yielded to Main
[14:27:21][LongRunningTasks Thread:4][Main] > Main ==> Completed in 5274 milliseconds
更复杂
好的,现在更接近现实并编写一些代码。
JobRunner
JobRunner是运行和控制异步作业的简单类。出于我们的目的,它运行长时间运行的任务之一来模拟工作,但是您可以将基本模式用于现实情况。
这是不言而喻的,但我将介绍TaskCompletionSource。
引用MS“代表未绑定到委托的Task<TResult>的生产方,通过Task属性提供对消费方的访问。”您可以通过TaskCompletionSource.Task实例获得由TaskCompletionSource.Task公开的Task,换句话说,是从方法中分离的手动控制Task。
Task表示JobRunner的状态作为JobTask属性而暴露。如果底层TaskCompletionSource没有设置它返回一个简单的Task.CompletedTask对象,否则返回JobTaskController的Task。该Run方法使用异步事件模式——我们需要异步运行的代码块,以进行控制await。Run控制Task状态,但其Task本身是独立于Run的。IsRunning确保作业一旦运行就无法再启动。
class JobRunner
{
public enum JobType { IO, Processor, YieldingProcessor }
public JobRunner(string name, int secs, JobType type = JobType.IO)
{
this.Name = name;
this.Seconds = secs;
this.Type = type;
}
public string Name { get; private set; }
public int Seconds { get; private set; }
public JobType Type { get; set; }
private bool IsRunning;
public Task JobTask => this.JobTaskController == null ?
Task.CompletedTask : this.JobTaskController.Task;
private TaskCompletionSource JobTaskController { get; set; } = new TaskCompletionSource();
public async void Run()
{
if (!this.IsRunning) {
this.IsRunning = true;
this.JobTaskController = new TaskCompletionSource();
switch (this.Type)
{
case JobType.Processor:
await LongRunningTasks.RunLongProcessorTaskAsync
(Seconds, Program.LogToConsole, Name);
break;
case JobType.YieldingProcessor:
await LongRunningTasks.RunYieldingLongProcessorTaskAsync
(Seconds, Program.LogToConsole, Name);
break;
default:
await LongRunningTasks.RunLongIOTaskAsync
(Seconds, Program.LogToConsole, Name);
break;
}
this.JobTaskController.TrySetResult();
this.IsRunning = false;
}
}
}
JobScheduler
JobScheduler是用于实际调度作业的方法。与Main分开来演示异步编程的一些关键行为。
- Stopwatch 提供时间安排。
- 创建四个不同的IO作业。
- 开始四个工作。
- 使用Task.WhenAll等待某些任务再继续。注意Task是JobRunnner实例公开的JobTask。
“WhenAll”是几种静态“Task”方法之一。whenAll创建一个单个Task,它唤醒提交数组中的所有Task。所有任务完成后,其状态将更改为“Complete”。`WhenAny`很类似,但是当完成时将被设置为*Complete *。它们可以被命名为*AwaitAll*和*AwaitAny*。“WaitAll”和“WaitAny”是阻止版本,类似于“Wait”。不知道命名转换有点令人困惑的原因——我敢肯定有一个。
static async Task JobScheduler()
{
var watch = new Stopwatch();
watch.Start();
var name = "Job Scheduler";
var quickjob = new JobRunner("Quick Job", 3);
var veryslowjob = new JobRunner("Very Slow Job", 7);
var slowjob = new JobRunner("Slow Job", 5);
var veryquickjob = new JobRunner("Very Quick Job", 2);
quickjob.Run();
veryslowjob.Run();
slowjob.Run();
veryquickjob.Run();
UILogger.LogToUI(LogToConsole, $"All Jobs Scheduled", name);
await Task.WhenAll(new Task[] { quickjob.JobTask, veryquickjob.JobTask }); ;
UILogger.LogToUI(LogToConsole, $"Quick Jobs completed in
{watch.ElapsedMilliseconds} milliseconds", name);
await Task.WhenAll(new Task[] { slowjob.JobTask, quickjob.JobTask,
veryquickjob.JobTask, veryslowjob.JobTask }); ;
UILogger.LogToUI(LogToConsole, $"All Jobs completed in
{watch.ElapsedMilliseconds} milliseconds", name);
watch.Stop();
}
现在,我们需要对Main做一些改变:
static async Task Main(string[] args)
{
var watch = new Stopwatch();
watch.Start();
UILogger.LogThreadType(LogToConsole, "Main");
var task = JobScheduler();
UILogger.LogToUI(LogToConsole, $"Job Scheduler yielded to Main", "Main");
await task;
UILogger.LogToUI(LogToConsole, $"final yield to Main", "Main");
watch.Stop();
UILogger.LogToUI(LogToConsole, $"Main ==> Completed in
{ watch.ElapsedMilliseconds} milliseconds", "Main");
//return Task.CompletedTask;
}
运行此命令时,将在下面显示输出。需要注意的有趣的地方是:
- 每个作业都开始,然后在第一次等待时产生,将控制权交还给调用者——在这种情况下为JobSchedular。
- JobScheduler运行到第一个await并返回到Main。
- 当头两个作业完成时,它们的JobTask将设置为完成并JobScheduler继续进行下一个await作业。
- JobScheduler在最长的时间上需要一点时间完成Job。
[16:58:52][Main Thread:1][Main] > running on Application Thread
[16:58:52][Main Thread:1][LongRunningTasks] > Quick Job started
[16:58:52][Main Thread:1][LongRunningTasks] > Very Slow Job started
[16:58:52][Main Thread:1][LongRunningTasks] > Slow Job started
[16:58:52][Main Thread:1][LongRunningTasks] > Very Quick Job started
[16:58:52][Main Thread:1][Job Scheduler] > All Jobs Scheduled
[16:58:52][Main Thread:1][Main] > Job Scheduler yielded to Main
[16:58:54][LongRunningTasks Thread:4][LongRunningTasks] >
Very Quick Job completed in 2022 millisecs
[16:58:55][LongRunningTasks Thread:4][LongRunningTasks] >
Quick Job completed in 3073 millisecs
[16:58:55][LongRunningTasks Thread:4][Job Scheduler] >
Quick Jobs completed in 3090 milliseconds
[16:58:57][LongRunningTasks Thread:4][LongRunningTasks] >
Slow Job completed in 5003 millisecs
[16:58:59][LongRunningTasks Thread:6][LongRunningTasks] >
Very Slow Job completed in 7014 millisecs
[16:58:59][LongRunningTasks Thread:6][Job Scheduler] >
All Jobs completed in 7111 milliseconds
[16:58:59][LongRunningTasks Thread:6][Main] > final yield to Main
[16:58:59][LongRunningTasks Thread:6][Main] > Main ==> Completed in 7262 milliseconds
现在将作业类型更改为Processor如下:
var quickjob = new JobRunner("Quick Job", 3, JobRunner.JobType.Processor);
var veryslowjob = new JobRunner("Very Slow Job", 7, JobRunner.JobType.Processor);
var slowjob = new JobRunner("Slow Job", 5, JobRunner.JobType.Processor);
var veryquickjob = new JobRunner("Very Quick Job", 2, JobRunner.JobType.Processor);
运行此命令时,您会看到所有操作都在Main Thread上按顺序运行。首先,您认为为什么?我们有多个可用线程,并且Scheduler展示了其在线程之间切换任务的能力。为什么不切换?
答案很简单。初始化JobRunnner对象后,我们一次将它们运行到一个Scheduler对象中。由于我们运行的代码是顺序的——不间断地计算素数——在第一个作业完成之前,我们不执行下一行代码(输入第二个作业)。
[17:59:48][Main Thread:1][Main] > running on Application Thread
[17:59:48][Main Thread:1][LongRunningTasks] > Quick Job started
[17:59:53][Main Thread:1][LongRunningTasks] > Quick Job completed in 4355 millisecs
[17:59:53][Main Thread:1][LongRunningTasks] > Very Slow Job started
[17:59:59][Main Thread:1][LongRunningTasks] > Very Slow Job completed in 6057 millisecs
[17:59:59][Main Thread:1][LongRunningTasks] > Slow Job started
[18:00:03][Main Thread:1][LongRunningTasks] > Slow Job completed in 4209 millisecs
[18:00:03][Main Thread:1][LongRunningTasks] > Very Quick Job started
[18:00:05][Main Thread:1][LongRunningTasks] > Very Quick Job completed in 1737 millisecs
[18:00:05][Main Thread:1][Job Scheduler] > All Jobs Scheduled
[18:00:05][Main Thread:1][Job Scheduler] > Quick Jobs completed in 16441 milliseconds
[18:00:05][Main Thread:1][Job Scheduler] > All Jobs completed in 16441 milliseconds
[18:00:05][Main Thread:1][Main] > Job Scheduler yielded to Main
[18:00:05][Main Thread:1][Main] > final yield to Main
[18:00:05][Main Thread:1][Main] > Main ==> Completed in 16591 milliseconds
现在,将作业更改为可以运行YieldingProcessor。
var quickjob = new JobRunner("Quick Job", 3, JobRunner.JobType.YieldingProcessor);
var veryslowjob = new JobRunner("Very Slow Job", 7, JobRunner.JobType.YieldingProcessor);
var slowjob = new JobRunner("Slow Job", 5, JobRunner.JobType.YieldingProcessor);
var veryquickjob = new JobRunner("Very Quick Job", 2, JobRunner.JobType.YieldingProcessor);
结果是非常不同的。花费的时间将取决于计算机上处理器核心和线程的数量。您可以看到所有作业快速启动并在11秒内完成,最慢的作业需要9秒。此处的主要区别在于,处理器长时间运行的工作有规律地产生。这使调度程序有机会将工作分流到其他线程。
Yield处理器代码:
[17:50:12][Main Thread:1][Main] > running on Application Thread
[17:50:12][Main Thread:1][LongRunningTasks] > Quick Job started
[17:50:12][Main Thread:1][LongRunningTasks] > Very Slow Job started
[17:50:12][Main Thread:1][LongRunningTasks] > Slow Job started
[17:50:12][Main Thread:1][LongRunningTasks] > Very Quick Job started
[17:50:12][Main Thread:1][Job Scheduler] > All Jobs Scheduled
[17:50:12][Main Thread:1][Main] > Job Scheduler yielded to Main
[17:50:16][LongRunningTasks Thread:7][LongRunningTasks] >
Very Quick Job completed in 4131 millisecs
[17:50:18][LongRunningTasks Thread:7][LongRunningTasks] >
Quick Job completed in 6063 millisecs
[17:50:18][LongRunningTasks Thread:7][Job Scheduler] >
Quick Jobs completed in 6158 milliseconds
[17:50:21][LongRunningTasks Thread:6][LongRunningTasks] >
Slow Job completed in 9240 millisecs
[17:50:23][LongRunningTasks Thread:9][LongRunningTasks] >
Very Slow Job completed in 11313 millisecs
[17:50:23][LongRunningTasks Thread:9][Job Scheduler] >
All Jobs completed in 11411 milliseconds
[17:50:23][LongRunningTasks Thread:9][Main] > final yield to Main
[17:50:23][LongRunningTasks Thread:9][Main] > Main ==> Completed in 11534 milliseconds
结论和总结
希望对您有帮助/信息丰富的?我在异步路线上的航行中学到了一些关键点,并在此处进行了演示:
- 一路异步等待。不要混用同步和异步方法。从底部开始——数据或流程接口——并通过数据和业务/逻辑层一直到UI一直进行代码异步。
- 如果不yield,您将无法异步运行。您必须给任务计划者一个机会!在Task中包装一些同步例程是在说而不是在做。
- 触发并遗忘void返回方法需要yield,以将控制权传递回调用方。它们的行为与任务返回方法没有什么不同。他们只是不为您返回任务以等待或监视进度。
- 如果您正在编写处理器密集型活动——建模、大量运算...请确保使其异步并在适当的位置yield。考虑将它们切换到任务池(请考虑以下注意事项)。测试不同的场景,没有一成不变的规则。
- 仅在UI中使用Task.Run,就在调用堆栈的顶部。永远不要在库中使用它。除非有充分的理由,否则不要使用它。
- 在awaits上使用记录和断点查看何时击中它们。您的代码以多快的速度回落到await外部是很好的响应能力指标。拿出你的外面await,看看你有多快掉到底部!
- 您可能已经注意到没有ContinueWith。我不经常使用它。通常,简单的await后跟延续代码可以达到相同的结果。我读过评论说它在处理上比较重,因为它创建了一个新任务,而等待/继续重复使用了相同的Task。我还没有对代码进行足够深入的研究。
- 始终使用Async和Await,不要花哨。
- 如果您的库同时提供了异步调用和同步调用,请分别对它们进行编码。“一次性编码”最佳实践不适用于此处。如果您不想在某个时候搬起石头砸自己的脚,就永远不要循环调用!
https://www.codeproject.com/Articles/5293229/A-Practical-Walkthrough-of-Async-Programming-in-Do