C#中的异步/等待:概念,内部设计,有用的技巧

大家好 这次,我们将讨论一个有关C#语言的每一个自尊自重的人都开始理解的主题-使用Task进行异步编程,或者通常情况下,使用async / await。 Microsoft做得很好-为了在大多数情况下使用异步,您只需要了解语法,而无需其他细节。 但是,如果深入探讨,该主题将非常繁复。 许多人都以自己的风格陈述了这一点。 关于这个话题有很多很酷的文章,但是围绕它仍然有很多误解。 我们将尽力纠正这种情况并尽可能地咀嚼材料,而不会牺牲深度或理解力。



涵盖的主题/章节:

  1. 异步的概念-异步的好处以及有关“阻塞”线程的神话
  2. TAP。 语法和编译条件 -编写编译方法的前提条件
  3. 使用TAP进行工作 -异步代码中程序的机制和行为(释放线程,启动任务并等待它们完成)
  4. 幕后花絮:状态机 -编译器转换及其生成的类的概述
  5. 异步的起源。 标准异步方法的设备 -用于从内部处理文件和网络的异步方法
  6. TAP类和技巧是有用的技巧,可以帮助您使用TAP管理和加速程序

异步概念


异步本身并不是什么新鲜事物。 异步通常意味着以不暗示阻塞调用线程的方式执行操作,即在不等待其完成的情况下开始操作。 阻止并不像所描述的那样邪恶。 可能有人提出过这样的说法,即阻塞的线程浪费了CPU时间,工作更慢并导致下雨。 后者似乎不太可能吗? 实际上,前2点是相同的。

在OS调度程序级别,当线程处于“阻塞”状态时,将不会为其分配宝贵的处理器时间。 调度程序调用通常属于导致阻塞,计时器中断和其他中断的操作。 也就是说,例如,当磁盘控制器完成读取操作并启动适当的中断时,调度程序将启动。 他将决定是启动被该操作阻止的线程,还是启动其他优先级更高的线程。

慢工作似乎更加荒谬。 实际上,实际上,这项工作是一回事。 只有异步操作会增加一些额外的开销。

雨水的挑战通常不是该地区的问题。

主要的阻塞问题是计算机资源的不合理消耗。 即使我们忘记了创建线程并使用线程池的时间,每个阻塞的线程也会占用额外的空间。 好吧,在某些情况下,只有一个线程可以执行某些工作(例如,UI线程)。 因此,我不希望他忙于另一个线程可以执行的任务,而牺牲了它专有的操作性能。

异步是一个非常广泛的概念,可以通过多种方式实现。
.NET的历史可以区分以下几点

  1. EAP(基于事件的异步模式)-顾名思义,加息基于操作完成时触发的事件以及调用此操作的常用方法
  2. APM(异步编程模型)-基于2种方法。 BeginSmth方法返回IAsyncResult接口。 EndSmth方法接受IAsyncResult(如果在调用EndSmth时操作尚未完成,则线程被阻塞)
  3. TAP(基于任务的异步模式)与异步/等待相同(严格来说,这些词出现在方法和任务类型和任务<TResult>出现之后,但是异步/等待显着改善了此概念)

后一种方法是如此成功,以至于每个人都成功地忘记了前一种方法。 因此,将与他有关。

基于任务的异步模式。 语法和编译条件


标准的TAP风格的异步方法非常易于编写。

为此,您需要

  1. 对于返回值为Task,Task <T>或void(不推荐,稍后讨论)。 在C#7中出现了类似任务的类型(在上一章中进行了讨论)。 在C#8中,将IAsyncEnumerable <T>和IAsyncEnumerator <T>添加到此列表中。
  2. 这样,该方法将用async关键字标记,并在内部包含await。 这些关键字是配对的。 而且,如果该方法包含await,请确保将其标记为async,反之则不成立,但没有意义。
  3. 为了礼貌起见,请遵守Async后缀约定。 当然,编译器不会将其视为错误。 如果您是一个非常体面的开发人员,则可以使用CancellationToken添加重载(在上一章中进行了讨论)

对于此类方法,编译器会认真工作。 它们在幕后变得完全无法识别,但稍后会更多。

提到该方法应包含await关键字。 它(单词)表示需要异步等待要执行的任务,这是应用该任务的任务对象。

任务对象还具有某些条件,因此可以对其应用等待:

  1. 期望的类型必须具有公共(或内部)GetAwaiter()方法,它也可以是扩展方法。 此方法返回一个等待对象。
  2. 等待对象必须实现INotifyCompletion接口,该接口需要实现void OnCompleted(动作继续)方法。 它还应该具有实例属性bool IsCompleted,它是无效的GetResult()方法。 它可以是结构或类。

下面的示例显示了如何使一个int预期,甚至从未执行过。

扩展int
public class Program { public static async Task Main() { await 1; } } public static class WeirdExtensions { public static AnyTypeAwaiter GetAwaiter(this int number) => new AnyTypeAwaiter(); public class AnyTypeAwaiter : INotifyCompletion { public bool IsCompleted => false; public void OnCompleted(Action continuation) { } public void GetResult() { } } } 



使用TAP


不了解某事应该如何工作就很难进入丛林。 在程序行为方面考虑TAP。

用术语:正在考虑的异步方法(将考虑其代码),我将调用异步方法 ,并且在其中调用的异步方法将称为异步操作

让我们以最简单的示例为例,我们采用Task.Delay作为异步操作,该操作延迟指定的时间而不会阻塞流。

 public static async Task DelayOperationAsync() //   { BeforeCall(); Task task = Task.Delay(1000); //  AfterCall(); await task; AfterAwait(); } 

就行为而言,该方法的执行如下。

  1. 执行异步操作之前的所有代码。 在这种情况下,这是BeforeCall方法
  2. 正在进行异步操作调用。 在此阶段,线程不会被释放或阻塞。 此操作返回结果-提及的任务对象(通常是Task),存储在局部变量中
  3. 该代码在调用异步操作之后但在等待(await)之前执行。 在示例中-AfterCall
  4. 等待任务对象(存储在本地变量中)的完成-等待任务。

    如果此时异步操作完成,则在同一线程中同步继续执行。

    如果异步操作未完成,则将保存必须在异步操作完成时调用的代码(所谓的延续),并且流返回线程池并变得可用。
  5. 等待后执行操作-AfterAwait-在等待时完成操作的同一线程中立即执行,或者在操作完成后采用将继续的新线程(在上一步中保存)


在幕后。 状态机


实际上,编译器将我们的方法转换为存根方法,在该方法中,将初始化生成的类(状态机)。 然后,它(机器)启动,并从方法返回步骤2中使用的Task对象。

特别有趣的是状态机的MoveNext方法。 此方法执行异步方法中转换之前的操作。 它将在每个等待调用之间中断代码。 每个部分都是在机器的特定条件下执行的。 MoveNext方法本身作为续集附加到等待对象。 状态的保存保证了逻辑上遵循期望的那一部分的执行。

正如他们所说,看1次总比听100次好,因此,我强烈建议您熟悉以下示例。 我重新编写了一些代码,改进了变量命名,并慷慨地发表了评论。

源代码
 public static async Task Delays() { Console.WriteLine(1); await Task.Delay(1000); Console.WriteLine(2); await Task.Delay(1000); Console.WriteLine(3); await Task.Delay(1000); Console.WriteLine(4); await Task.Delay(1000); Console.WriteLine(5); await Task.Delay(1000); } 


存根法
 [AsyncStateMachine(typeof(DelaysStateMachine))] [DebuggerStepThrough] public Task Delays() { DelaysStateMachine stateMachine = new DelaysStateMachine(); stateMachine.taskMethodBuilder = AsyncTaskMethodBuilder.Create(); stateMachine.currentState = -1; AsyncTaskMethodBuilder builder = stateMachine.taskMethodBuilder; taskMethodBuilder.Start(ref stateMachine); return stateMachine.taskMethodBuilder.Task; } 


状态机
 [CompilerGenerated] private sealed class DelaysStateMachine : IAsyncStateMachine { //  ,     await   //       await'a public int currentState; public AsyncTaskMethodBuilder taskMethodBuilder; //   private TaskAwaiter taskAwaiter; //  ,             ""  public int paramInt; private int localInt; private void MoveNext() { int num = currentState; try { TaskAwaiter awaiter5; TaskAwaiter awaiter4; TaskAwaiter awaiter3; TaskAwaiter awaiter2; TaskAwaiter awaiter; switch (num) { default: localInt = paramInt; //  await Console.WriteLine(1); //  await awaiter5 = Task.Delay(1000).GetAwaiter(); //  await if (!awaiter5.IsCompleted) //  await. ,    { num = (currentState = 0); // ,      taskAwaiter = awaiter5; //    ,        DelaysStateMachine stateMachine = this; //    taskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter5, ref stateMachine); //                 return; } goto Il_AfterFirstAwait; //  ,   ,    case 0: //            ,        .   ,          awaiter5 = taskAwaiter; //   taskAwaiter = default(TaskAwaiter); //   num = (currentState = -1); //  goto Il_AfterFirstAwait; //       case 1: //  ,      ,    ,     . awaiter4 = taskAwaiter; taskAwaiter = default(TaskAwaiter); num = (currentState = -1); goto Il_AfterSecondAwait; case 2: // ,     . awaiter3 = taskAwaiter; taskAwaiter = default(TaskAwaiter); num = (currentState = -1); goto Il_AfterThirdAwait; case 3: //    awaiter2 = taskAwaiter; taskAwaiter = default(TaskAwaiter); num = (currentState = -1); goto Il_AfterFourthAwait; case 4: //    { awaiter = taskAwaiter; taskAwaiter = default(TaskAwaiter); num = (currentState = -1); break; } Il_AfterFourthAwait: awaiter2.GetResult(); Console.WriteLine(5); //     awaiter = Task.Delay(1000).GetAwaiter(); //   if (!awaiter.IsCompleted) { num = (currentState = 4); taskAwaiter = awaiter; DelaysStateMachine stateMachine = this; taskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); return; } break; Il_AfterFirstAwait: //  ,        awaiter5.GetResult(); //       Console.WriteLine(2); //  ,     await awaiter4 = Task.Delay(1000).GetAwaiter(); //    if (!awaiter4.IsCompleted) { num = (currentState = 1); taskAwaiter = awaiter4; DelaysStateMachine stateMachine = this; taskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter4, ref stateMachine); return; } goto Il_AfterSecondAwait; Il_AfterThirdAwait: awaiter3.GetResult(); Console.WriteLine(4); //     awaiter2 = Task.Delay(1000).GetAwaiter(); //   if (!awaiter2.IsCompleted) { num = (currentState = 3); taskAwaiter = awaiter2; DelaysStateMachine stateMachine = this; taskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter2, ref stateMachine); return; } goto Il_AfterFourthAwait; Il_AfterSecondAwait: awaiter4.GetResult(); Console.WriteLine(3); //     awaiter3 = Task.Delay(1000).GetAwaiter(); //   if (!awaiter3.IsCompleted) { num = (currentState = 2); taskAwaiter = awaiter3; DelaysStateMachine stateMachine = this; taskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter3, ref stateMachine); return; } goto Il_AfterThirdAwait; } awaiter.GetResult(); } catch (Exception exception) { currentState = -2; taskMethodBuilder.SetException(exception); return; } currentState = -2; taskMethodBuilder.SetResult(); //    ,   ,       } void IAsyncStateMachine.MoveNext() {...} [DebuggerHidden] private void SetStateMachine(IAsyncStateMachine stateMachine) {...} void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) {...} } 


我将重点放在短语“此时尚未同步执行”。 异步操作也可以遵循同步执行路径。 当前异步方法要同步执行(即不更改线程)的主要条件是IsCompleted验证时异步操作的完成。

此示例清楚地说明了此行为。
 static async Task Main() { Console.WriteLine(Thread.CurrentThread.ManagedThreadId); //1 Task task = Task.Delay(1000); Thread.Sleep(1700); await task; Console.WriteLine(Thread.CurrentThread.ManagedThreadId); //1 } 


关于同步上下文。 机器中使用的AwaitUnsafeOnCompleted方法最终导致对Task.SetContinuationForAwait方法的调用。 在此方法中,检索当前的同步上下文SynchronizationContext.Current 。 同步上下文可以解释为一种流。 如果它也是特定的(例如,UI线程的上下文),则使用SynchronizationContextAwaitTaskContinuation类创建一个延续。 启动继续的此类在保存的上下文上调用Post方法,以确保继续在运行该方法的确切上下文中执行。 缓和地说,执行速度的具体逻辑取决于上下文中的Post方法。 如果没有同步上下文(或者表明对我们而言无关紧要,将使用ConfigureAwait(false)继续执行在哪个上下文中,这将在上一章中进行讨论),则继续将由线程从池中执行。

异步的起源。 设备标准异步方法


我们研究了使用异步和等待的方法的外观以及幕后发生的情况。 此信息并不罕见。 但是了解异步操作的性质很重要。 因为,正如我们在状态机中看到的那样,除非在操作中更巧妙地处理了异步操作,否则代码中会调用异步操作。 但是,异步操作本身内部会发生什么? 大概是一样的,但是这不可能无限期地发生。

重要的任务是了解异步的性质。 当试图理解异步时,状态“现在清楚”和“现在又变得不可理解”交替出现。 并且这种交替将一直持续到理解异步源为止。

在使用异步时,我们会执行任务。 这与流完全不同。 一个任务可以由许多线程执行,而一个线程可以执行许多任务。

异步通常从返回Task的方法开始(例如),但没有使用async标记,因此在内部不使用await。 此方法不能容忍任何编译器更改;它按原样执行。

因此,让我们看一下异步的某些根源。

  1. Task.Run,​​新Task(..),Start(),Factory.StartNew等。 启动异步执行的最简单方法。 这些方法只是创建一个新的任务对象,并将委托作为参数之一传递。 该任务被传输到调度程序,该调度程序使其可以由池中的线程之一执行。 返回可以完成的任务。 通常,此方法用于在单独的线程中开始计算(CPU绑定)。
  2. TaskCompletionSource。 一个帮助程序类,可以帮助控制任务对象。 专为那些无法为实现分配委托并使用更复杂的机制来控制完成的人而设计。 它有一个非常简单的API-SetResult,SetError等,它们会相应地更新任务。 可通过Task属性使用此任务。 也许您将在内部创建线程,对它们的交互或按事件完成具有复杂的逻辑。 关于此类的更多详细信息将在最后一部分中。

在另一段中,您可以使用标准库的方法。 这些包括读取/写入文件,使用网络等。 通常,这种流行和通用的方法使用在不同平台上有所不同的系统调用,并且它们的设备非常有趣。 考虑使用文件和网络。

档案


重要说明-如果要使用文件,则在创建FileStream时必须指定useAsync = true。

一切都平凡而混乱地排列在文件中。 FileStream类声明为部分。 除此之外,还有6个特定于平台的附加组件。 因此,在Unix中,通常,对任意文件的异步访问在单独的线程中使用同步操作。 在Windows中,当然有用于异步操作的系统调用。 这导致在不同平台上工作的差异。 资料来源

Unix系统

如果缓冲区允许并且流不忙于其他操作,则写入或读取时的标准行为是同步执行该操作:

1.流不忙于其他操作

Filestream类具有一个从SemaphoreSlim继承的对象,该对象的参数为(1,1)-即一个关键部分-受该信号量保护的代码片段一次只能由一个线程执行。 此信号量可用于读取和写入。 即,不可能同时产生读取和写入。 在这种情况下,不会发生信号量阻塞。 在此方法上调用this._asyncState.WaitAsync()方法,该方法返回一个任务对象(没有锁或等待,这是如果将await关键字应用于该方法的结果)。 如果此任务对象未完成-即捕获了信号量,则将在其中执行操作的继续(Task.ContinueWith)附加到返回的等待对象。 如果对象是免费的,则需要检查以下内容

2.缓冲区允许

这里的行为已经取决于操作的性质。

对于记录-检查文件中写入数据+位置的数据大小是否小于缓冲区的大小,默认情况下为4096字节。 也就是说,我们必须从头开始写4096字节,从2048开始偏移2048字节,依此类推。 如果是这种情况,则将同步执行该操作,否则将附加延续(Task.ContinueWith)。 续集使用常规的同步系统调用。 当缓冲区已满时,它将被同步写入磁盘。
为了读取-检查缓冲区中是否有足够的数据以便返回所有必需的数据。 如果不是,则再次使用同步系统调用继续执行(Task.ContinueWith)。

顺便说一下,有一个有趣的细节。 如果一条数据占据了整个缓冲区,则它们将直接写入文件,而无需缓冲区的参与。 同时,存在这样一种情况:数据量将超过缓冲区的大小,但它们将全部通过缓冲区。 如果缓冲区中已经有东西,则会发生这种情况。 然后,我们的数据将分为两部分,一部分将填充到缓冲区的最后,数据将被写入文件,第二部分将被写入缓冲区(如果未进入)或直接写入文件(如果没有)。 因此,如果我们创建一个流并向其中写入4097个字节,它们将立即出现在文件中,而无需调用Dispose。 如果我们写4095,则文件中将没有任何内容。

窗户

在Windows下,直接使用缓冲区和写入的算法非常相似。 但是,在异步系统的写和读调用中直接观察到了显着差异。 在不深入探讨系统调用的情况下,存在这样的重叠结构。 对我们来说,它有一个重要的领域-HANDLE hEvent。 这是一个手动重置事件,在操作完成后会进入警报状态。 返回执行。 直接写入以及写入缓冲区都使用异步系统调用,该系统调用将上述结构用作参数。 录制时,将创建一个FileStreamCompletionSource对象-TaskCompletionSource的继承者,其中指定了IOCallback。 操作完成后,池中的空闲线程会调用它。 在回调中,将解析Overlapped结构,并相应地更新Task对象。 太神奇了。

联播网


很难描述我所了解的一切。 我的路径是从HttpClient到Socket,再到Unix的SocketAsyncContext。 通用方案与文件相同。 对于Windows,使用提到的Overlapped结构,并且该操作异步执行。 在Unix上,网络操作也使用回调函数。

还有一点解释。 细心的读者会注意到,在调用和回调之间使用异步调用时,某些空虚会以某种方式处理数据。 为了完整起见,有必要澄清一下。 在文件示例中,磁盘控制器通过磁盘控制器对磁盘执行直接操作,是发出有关将磁头移动到所需扇区等的信号的人。 处理器此时是免费的。 通过输入/输出端口与磁盘进行通信。 它们指示操作的类型,数据在磁盘上的位置等。 接下来,控制器和磁盘将参与此操作,并在工作完成后生成中断。 因此,异步系统调用仅向输入/输出端口提供信息,而同步系统也等待结果,从而将流置于阻塞状态。 这种方案并不假装绝对准确(与本文无关),但提供了对工作的概念性理解。

现在,该过程的性质很明确。 但是有人会问,如何处理异步? 永远无法通过方法编写异步信息。

首先。 可以将应用程序作为服务进行。 在这种情况下,入口点-Main-由您从头开始编写。 直到最近,Main才可能是异步的;在该语言的版本7中,已添加了此功能。 但是它并没有什么根本的改变,只是编译器生成了通常的Main,然后从异步方法生成了一个静态方法,该方法在Main中被调用,并且它的完成可以同步进行。 因此,很可能您会采取一些长期的行动。 由于某些原因,此刻,许多人开始考虑如何为此业务创建线程:通常是通过Task,ThreadPool或Thread手动进行的,因为两者之间应该有所不同。 答案很简单-当然是Task。 如果您使用TAP方法,请不要干扰手动创建线程。 这类似于对几乎所有请求使用HttpClient,并且POST是通过Socket独立完成的。

其次。 Web应用程序。 每个传入请求都会从ThreadPool中提取一个新线程进行处理。 当然,池很大,但不是无限的。 在有大量请求的情况下,可能根本没有足够的线程,并且所有新请求都将排队等待处理。 这种情况称为饥饿。 但是,如前所述,在使用异步控制器的情况下,流返回到池中并可以用于处理新请求。 因此,大大提高了服务器的吞吐量。

我们从头到尾都研究了异步过程。 理解了所有与人类本性矛盾的异步之后,我们将在处理异步代码时考虑一些有用的技巧。

使用TAP时的有用的类和技巧


Task类的静态多样性。


Task类具有几个有用的静态方法。以下是主要内容。

  1. Task.WhenAny(..)是一个组合器,接受任务对象的IEnumerable / params,并返回一个任务对象,该任务对象将在完成第一个任务时完成。也就是说,它允许您等待几个正在运行的任务之一
  2. Task.WhenAll(..)-组合器,接受任务对象的IEnumerable / params并返回任务对象,该任务将在所有传输的任务完成后完成
  3. Task.FromResult<T>(T value) — , .
  4. Task.Delay(..) —
  5. Task.Yield() — . , . , ,

ConfigureAwait


自然,最流行的“高级”功能。此方法属于Task类,并允许您指定是否需要在调用异步操作的相同上下文中继续。默认情况下,不使用此方法,将使用提到的Post方法记住上下文并在其中继续。但是,正如我们所说,邮政是一种非常昂贵的享受。因此,如果性能处于第一位,并且我们看到继续操作不会更新UI,则可以在waiting对象上指定.ConfigureAwait(false)。这意味着在哪里继续执行对我们来说都没有关系。

现在来解决这个问题。正如他们所说,可怕不是无知,而是错误的知识。

我以某种方式偶然地观察了Web应用程序的代码,其中每个异步调用都使用此加速器进行修饰。除了视觉上的厌恶之外,这没有其他作用。标准的ASP.NET Core Web应用程序没有任何唯一的上下文(当然,除非您自己编写它们)。因此,无论如何都不会调用Post方法。

TaskCompletionSource <T>


一个易于管理Task对象的类。一个类有很多机会,但是当我们想用一个动作包装一个任务时,该类最有用。通常,创建该类是为了使旧的异步方法适应TAP,但正如我们所看到的,它不仅用于此目的。一个使用此类的小例子:

例子
 public static Task<string> GetSomeDataAsync() { TaskCompletionSource<string> tcs = new TaskCompletionSource<string>(); FileSystemWatcher watcher = new FileSystemWatcher { Path = Directory.GetCurrentDirectory(), NotifyFilter = NotifyFilters.LastAccess, EnableRaisingEvents = true }; watcher.Changed += (o, e) => tcs.SetResult(e.FullPath); return tcs.Task; } 


此类创建一个异步包装器,以获取在当前文件夹中访问的文件的名称。

CancellationTokenSource


允许您取消异步操作。总体轮廓类似于TaskCompletionSource的使用。首先,创建var cts = new CancellationTokenSource(),顺便说一下,它是IDisposable,然后将cts.Token传递给异步操作此外,按照您的某些逻辑,在某些条件下,将调用cts.Cancel()方法它还可以订阅事件或其他任何内容。

使用CancellationToken是一个好习惯。编写在后台执行某些工作的异步方法时(例如,无限长的时间),您只需在循环主体中插入一行即可:cancelleToken.ThrowIfCancellationRequested(),这将引发异常OperationCanceledException该异常被视为操作取消,并且不会保存为任务对象内的异常。同样,Task对象IsCanceled属性将变为true。

长时间运行


通常,在某些情况下,尤其是在编写服务时,创建几个将在服务的整个生命周期内或很长时间内起作用的任务。我们记得,使用线程池确实是创建线程的开销。但是,如果很少创建流(甚至每小时创建一次),则这些费用是固定的,您可以安全地创建单独的流。为此,在创建任务时,可以指定一个特殊选项:

Task.Factory.StartNew(action,TaskCreationOptions.LongRunning

无论如何,我建议您查看所有task.Factory.StartNew重载,有很多方法可以灵活地配置任务以满足特定需求。

例外情况


由于异步代码执行的不确定性,因此异常问题非常重要。如果您无法捕获该异常并将其抛出在左线程中,则将杀死进程,这将是一种耻辱。创建ExceptionDispatchInfo类可以在一个线程中捕获异常并将其抛出异常。为了捕获异常,使用了静态方法ExceptionDispatchInfo.Capture(ex),该方法返回ExceptionDispatchInfo。可以将此对象的链接传递到任何线程,然后该线程调用Throw()方法将其丢弃。引发本身不会在异步操作调用的地方发生,而是在使用await运算符的地方发生。如您所知,等待不能应用于无效。因此,如果上下文存在,它将通过Post方法传递给它。否则,它将在池中的流中被激发。这几乎是应用程序崩溃的100%问候。这是我们应该使用Task或Task <T>而不是无效的事实的实践。

还有一件事。有一个活动策划TaskScheduler.UnobservedTaskException,它扔在触发UnobservedTaskException当GC尝试收集具有未处理异常的任务对象时,将在垃圾回收期间引发此异常。

IAsyncEnumerable


在C#8和.NET Core 3.0之前,无法在异步方法中使用yield迭代器,这会复杂化生命并使它从该方法返回Task <IEnumerable <T >>,即在完全收到收藏之前,无法遍历整个收藏。现在有这样的机会。在此处了解更多信息。为此,返回类型必须为IAsyncEnumerable <T>(或IAsyncEnumerator <T>)。要遍历此类集合,应将foreach循环与await关键字一起使用。同样,可以在操作结果上调用WithCancellationConfigureAwait方法,指示使用CancellationToken需要在同一上下文中继续进行。

正如预期的那样,一切都尽可能地懒惰进行。
下面是一个例子和他给出的结论。

例子
 public class Program { public static async Task Main() { Stopwatch sw = new Stopwatch(); sw.Start(); IAsyncEnumerable<int> enumerable = AsyncYielding(); Console.WriteLine($"Time after calling: {sw.ElapsedMilliseconds}"); await foreach (var element in enumerable.WithCancellation(..).ConfigureAwait(false)) { Console.WriteLine($"element: {element}"); Console.WriteLine($"Time: {sw.ElapsedMilliseconds}"); } } static async IAsyncEnumerable<int> AsyncYielding() { foreach (var uselessElement in Enumerable.Range(1, 3)) { Task task = Task.Delay(TimeSpan.FromSeconds(uselessElement)); Console.WriteLine($"Task run: {uselessElement}"); await task; yield return uselessElement; } } } 


结论:

调用后的时间:0
任务运行:1
元素:1
时间:1033
任务运行:2
元素:2
时间:3034
任务运行:3
元素:3
时间:6035


线程池


使用TAP编程时,将积极使用此类。因此,我将给出其实现的最小细节。在内部,ThreadPool有一个队列数组:每个线程一个+全局一个。将新作业添加到池中时,将考虑启动添加的线程。如果它是池中的一个线程,则该工作将放在该线程的自己队列中(如果它是另一个线程)-在全局线程中。选择一个线程工作时,将首先查找其本地队列。如果为空,则线程从全局中获取作业。如果为空,它将开始从其他地方窃取。另外,您永远不应依赖工作的顺序,因为实际上没有顺序。池中的默认线程数取决于许多因素,包括地址空间的大小。如果还有更多执行请求,超过可用线程数时,请求将排队。

线程池中的线程是后台线程(属性isBackground = true)。如果所有前台线程均已完成,则此类型的线程不支持该进程的寿命。

系统线程监视等待句柄的状态。当等待操作结束时,传输的回调由线程从池中执行(请记住Windows中的文件)。

任务型


前面已经提到,这种类型(结构或类)可以用作异步方法的返回值。必须使用[AsyncMethodBuilder(..)]属性将构建器类型与此类型相关联为了能够将await关键字应用于此类型,该类型必须具有上述特征。可以对不返回值的方法进行参数化,对不返回值的方法进行参数化。

构建器本身是一个类或结构,其框架显示在下面的示例中。对于用T进行参数化的类似任务的类型SetResult方法的参数类型为T。对于非参数化类型,该方法没有参数。

所需的构建器界面
 class MyTaskMethodBuilder<T> { public static MyTaskMethodBuilder<T> Create(); public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine; public void SetStateMachine(IAsyncStateMachine stateMachine); public void SetException(Exception exception); public void SetResult(T result); public void AwaitOnCompleted<TAwaiter, TStateMachine>( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine; public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine; public MyTask<T> Task { get; } } 


下面将描述从编写类似任务的类型的角度出发的工作原理。解析编译器生成的代码时,已经描述了大多数。

编译器使用所有这些类型来生成状态机。编译器知道将哪些生成器用于其已知的类型,这里我们指定在代码生成期间将使用的生成器。如果状态机是结构,则在调用SetStateMachine时将对其进行打包,如果需要,构建器可以缓存打包的副本。生成器必须Start方法中或在调用后调用stateMachine.MoveNext才能开始执行并推进状态机。致电开始后,则Task属性将从方法中返回。我建议您返回到存根方法并查看这些步骤。

如果状态机成功完成,则调用SetResult方法,否则调用SetException。如果状态机到达等待状态,则执行任务类型GetAwaiter()方法。如果wait对象实现了ICriticalNotifyCompletion接口并且IsCompleted = false,则状态机使用builder.AwaitUnsafeOnCompleted(ref awaiter,ref stateMachine)AwaitUnsafeOnCompleted方法应调用awaiter.OnCompleted(操作),该操作应调用stateMachine.MoveNext等待对象完成时。对于INotifyCompletion接口builder.AwaitOnCompleted方法类似

如何使用它取决于您。但我建议您在生产中应用此功能之前,请考虑514次,而不是为了宠爱自己。以下是使用示例。我只画了一个标准构建器的代理,该代理会在控制台显示何时调用哪个方法。顺便说一句,异步Main()不想支持自定义类型的期望(我相信,由于Microsoft的这种遗漏,多个生产项目被无可救药地破坏了)。如果愿意,您可以使用普通记录器修改代理记录器并记录更多信息。

记录代理任务
 public class Program { public static void Main() { Console.WriteLine("Start"); JustMethod().Task.Wait(); //   Console.WriteLine("Stop"); } public static async LogTask JustMethod() { await DelayWrapper(1000); } public static LogTask DelayWrapper(int milliseconds) => new LogTask { Task = Task.Delay(milliseconds)}; } [AsyncMethodBuilder(typeof(LogMethodBuilder))] public class LogTask { public Task Task { get; set; } public TaskAwaiter GetAwaiter() => Task.GetAwaiter(); } public class LogMethodBuilder { private AsyncTaskMethodBuilder _methodBuilder = AsyncTaskMethodBuilder.Create(); private LogTask _task; public static LogMethodBuilder Create() { Console.WriteLine($"Method: Create; {DateTime.Now :O}"); return new LogMethodBuilder(); } public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine { Console.WriteLine($"Method: Start; {DateTime.Now :O}"); _methodBuilder.Start(ref stateMachine); } public void SetStateMachine(IAsyncStateMachine stateMachine) { Console.WriteLine($"Method: SetStateMachine; {DateTime.Now :O}"); _methodBuilder.SetStateMachine(stateMachine); } public void SetException(Exception exception) { Console.WriteLine($"Method: SetException; {DateTime.Now :O}"); _methodBuilder.SetException(exception); } public void SetResult() { Console.WriteLine($"Method: SetResult; {DateTime.Now :O}"); _methodBuilder.SetResult(); } public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine { Console.WriteLine($"Method: AwaitOnCompleted; {DateTime.Now :O}"); _methodBuilder.AwaitOnCompleted(ref awaiter, ref stateMachine); } public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine { Console.WriteLine($"Method: AwaitUnsafeOnCompleted; {DateTime.Now :O}"); _methodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); } public LogTask Task { get { Console.WriteLine($"Property: Task; {DateTime.Now :O}"); return _task ??= new LogTask {Task = _methodBuilder.Task}; } set => _task = value; } } 


结论:

启动
方法:创建;2019-10-09T17:55:13.7152733 + 03:00
方法:开始; 2019-10-09T17:55:13.7262226 + 03:00
方法:AwaitUnsafeOnCompleted; 2019-10-09T17:55:13.7275206 + 03:00
属性:任务; 2019-10-09T17:55:13.7292005 + 03:00
方法:SetResult; 2019-10-09T17:55:14.7297967 + 03:00
停止就


这样,谢谢大家

Source: https://habr.com/ru/post/zh-CN470830/


All Articles