感谢良好的评论0x1000000来完成了此翻译。

.NET Framework 4引入了System.Threading.Tasks空间以及Task类。 这种类型以及它生成的Task <TResult>已经等待了很长时间,直到它们被.NET中的标准识别为C#5中通过其async / await语句引入的异步编程模型的关键方面。 在本文中,我将讨论ValueTask / ValueTask <TResult>的新类型,这些类型旨在在应考虑内存分配开销的情况下提高异步方法的性能。
工作任务
任务扮演着不同的角色,但主要任务是“承诺”(promise),它是表示某些操作可能完成的对象。 您启动一个操作并为其获取一个Task对象,该对象将在操作完成后执行,这可以在同步模式下作为操作初始化的一部分发生(例如,接收缓冲区中已经存在的数据),而在异步模式下则在执行时您将获得Task(不是从缓冲区接收数据,而是非常快速地接收数据),或者在异步模式下,但是在拥有Task(从远程资源接收数据)之后,得到Task。 由于该操作可以异步结束,因此您可以阻止执行流程,等待结果(这通常使调用的异步性变得毫无意义),或者创建一个回调函数,该函数将在操作完成后被激活。 在.Net 4中,回调的创建是通过Task对象的ContinueWith方法实现的,该方法通过在执行Task之后接受委托函数来运行来显式演示此模型:
SomeOperationAsync().ContinueWith(task => { try { TResult result = task.Result; UseResult(result); } catch (Exception e) { HandleException(e); } });
但是在.NET Framework 4.5和C#5中,任务对象可以简单地由await运算符调用,这使得获取异步操作的结果变得容易,并且当操作以同步模式,快速异步或快速完成时,针对上述选项进行优化的生成代码将在所有情况下均能正常工作。与使callbacka异步:
TResult result = await SomeOperationAsync(); UseResult(result);
任务是一个非常灵活的类,它具有许多优点。 例如,您可以一次对任意数量的使用者执行多次等待。 您可以将其放入集合(字典)中,以备将来重复使用,以将其用作异步调用结果的缓存。 如果需要,您可以在等待Task完成的同时阻止执行。 您可以在Task对象(有时称为“组合器”)上编写并应用各种操作,例如,“ when any when”(异步)等待多个Task的首次完成。
但是在最常见的情况下,这种灵活性变得多余:只需调用异步操作并等待任务完成即可:
TResult result = await SomeOperationAsync(); UseResult(result);
在这里,我们不需要等待执行多次。 我们无需确保期望具有竞争力。 我们不需要执行同步锁定。 我们不会编写组合器。 我们只是在等待异步操作的完成。 最后,这就是我们编写同步代码的方式(例如,TResult结果= SomeOperation();),并且通常将其转换为async / await。
此外,Task具有潜在的弱点,尤其是在创建大量实例时,高吞吐量和高性能是关键要求-Task是一类。 这意味着任何需要执行Task的操作都被迫创建和放置一个对象,并且创建的对象越多,垃圾收集器(GC)所需的工作就越多,并且这项工作消耗了我们可以花在更多事情上的资源。有用的。
在许多情况下,运行时库和系统库有助于缓解此问题。 例如,如果我们编写这样的方法:
public async Task WriteAsync(byte value) { if (_bufferedCount == _buffer.Length) { await FlushAsync(); } _buffer[_bufferedCount++] = value; }
通常,缓冲区中将有足够的可用空间,并且该操作将同步执行。 发生这种情况时,无需执行任何应返回的Task任务,因为没有返回值,因此使用Task等效于返回空值(无效)的同步方法。 因此,环境可以简单地缓存一个非泛型Task并作为对任何同步完成的异步方法的执行结果一次又一次地使用它(可以通过Task.CompletedTask获得此缓存的单例)。 或者,例如,您编写:
public async Task<bool> MoveNextAsync() { if (_bufferedCount == 0) { await FillBuffer(); } return _bufferedCount > 0; }
通常,由于期望数据已经在缓冲区中,因此该方法只检查_bufferedCount的值,看它是否大于0,然后返回true; 并且仅当缓冲区中没有数据时,才需要执行异步操作。 而且,由于只有两个可能的布尔类型结果(true和false),所以仅需要两个可能的Task对象来表示这些结果,因此环境可以缓存这些对象并以相应的值返回它们而无需分配内存。 仅在异步完成的情况下,该方法才需要创建一个新的Task,因为将需要在知道操作结果之前将其返回。
该环境为其他一些类型提供了缓存,但是缓存所有可能的类型是不现实的。 例如,以下方法:
public async Task<int> ReadNextByteAsync() { if (_bufferedCount == 0) { await FillBuffer(); } if (_bufferedCount == 0) { return -1; } _bufferedCount--; return _buffer[_position++]; }
通常也会同步执行。 但是,与结果类型为Boolean的变体不同,此方法返回Int32,其值约为40亿,并且缓存Task <int>的所有变体将需要数百GB的内存。 该环境为Task <int>提供了一个小的缓存,但是一组非常有限的值,例如,如果此方法以返回值4同步完成(数据已经在缓冲区中),它将是一个缓存的Task,但是如果返回值42,则需要创建一个新的任务<int>,类似于调用Task.FromResult(42)。
许多库方法尝试通过提供自己的缓存来消除这种情况。 例如,MemoryStream.ReadAsync方法的.NET Framework 4.5中的重载在从内存中读取数据时总是同步结束。 ReadAsync返回Task <int>,其中Int32结果指示已读取多少字节。 此方法通常在循环中使用,每个调用通常具有相同的所需字节数,并且通常可以完全满足此需求。 因此,对于重复调用ReadAsync而言,可以合理地预期Task <int>将以与上一个调用相同的值同步返回。 因此,MemoryStream为在上一次成功调用中返回的一个对象创建一个缓存。 在下一个调用中,如果结果重复,它将返回缓存的对象,如果没有,则使用Task.FromResult创建一个新对象,将其保存到缓存中并返回。
但是,在许多其他情况下,操作是同步执行的,但是必须创建Task <TResult>对象。
ValueTask <TResult>和同步执行
所有这一切都需要在.NET Core 2.0中实现一种新类型,该新类型在.NET的早期版本中的NuGet System.Threading.Tasks.Extensions:ValueTask <TResult>包中可用。
ValueTask <TResult>是在.NET Core 2.0中创建的,其结构能够包装TResult和Task <TResult>。 这意味着可以从async方法返回它,并且如果该方法被同步成功执行,则无需在堆上放置任何对象:您可以简单地使用值TResult初始化此ValueTask <TResult>结构并返回它。 仅在异步执行的情况下,将放置Task <TResult>对象,ValueTask <TResult>将对其进行包装(为最小化结构的大小并优化成功执行的情况,以不支持的异常结尾的异步方法也将放置Task <TResult>,因此ValueTask <TResult>也仅包装Task <TResult>,并且不会附带用于存储Exception的附加字段。
基于此,像MemoryStream.ReadAsync这样的方法,但返回ValueTask <int>,不应处理缓存,而可以这样编写:
public override ValueTask<int> ReadAsync(byte[] buffer, int offset, int count) { try { int bytesRead = Read(buffer, offset, count); return new ValueTask<int>(bytesRead); } catch (Exception e) { return new ValueTask<int>(Task.FromException<int>(e)); } }
ValueTask <TResult>和异步执行
编写可以异步完成的异步方法而无需额外放置结果的能力是一个巨大的胜利。 这就是为什么在.NET Core 2.0中添加ValueTask <TResult>的原因,现在通过返回ValueTask <TResult>而不是Task <TResult>来宣布可能在需要性能的应用程序中使用的新方法。 例如,当我们向.NET Core 2.1添加Stream类的新ReadAsync重载时,为了能够传递Memory而不是byte [],我们将在其中返回ValueTask <int>类型。 以这种形式,可以以更少的内存分配来使用Stream对象(在其中,ReadAsync方法通常是同步执行的,如MemoryStream的前面的示例一样)。
但是,当我们使用带宽非常高的服务时,我们仍然希望尽可能避免内存分配,这意味着也要减少和消除异步执行路径上的内存分配。
在await模型中,对于任何异步完成的操作,我们都需要具有返回表示该操作可能完成的对象的能力:调用方需要重定向将在操作结束时启动的回调,并且这需要堆中的唯一对象,该对象可以用作该对象的传输通道。此特定操作。 同时,这并不意味着该对象在操作完成后是否将被重用。 如果可以重复使用此对象,则API可以为一个或多个这些对象组织一个缓存,并将其用于顺序操作,即不要将同一个对象用于多个中间异步操作,而是将其用于非竞争性访问。
在.NET Core 2.1中,ValueTask <TResult>类已得到增强,以支持类似的池化和重用。 修订后的类不仅可以包装TResult或Task <TResult>,还可以包装新的IValueTaskSource <TResult>接口。 该接口提供了与ValueTask <TResult>对象一起进行异步操作所需的基本功能,与Task <TResult>的执行方式相同:
public interface IValueTaskSource<out TResult> { ValueTaskSourceStatus GetStatus(short token); void OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags); TResult GetResult(short token); }
GetStatus方法用于实现ValueTask <TResult> .IsCompleted之类的属性,该属性将返回有关执行异步操作还是完成异步操作以及完成方式(成功与否)的信息。 等待对象使用OnCompleted方法附加一个回调,以在操作完成后从等待点继续执行。 并且需要GetResult方法来获取操作的结果,因此在操作结束后,调用方可以获取TResult对象或传递任何引发的异常。
大多数开发人员不需要此接口:方法仅返回ValueTask <TResult>对象,可以将其创建为实现此接口的对象的包装,而调用方法将保持在黑暗状态。 该接口适用于在使用性能关键型API时需要避免分配内存的开发人员。
.NET Core 2.1中有几个这样的API的示例。 最著名的方法是Socket.ReceiveAsync和Socket.SendAsync,例如在2.1中添加了新的重载。
public ValueTask<int> ReceiveAsync(Memory<byte> buffer, SocketFlags socketFlags, CancellationToken cancellationToken = default);
此重载返回ValueTask <int>。 如果操作同步完成,则可以简单地返回带有相应值的ValueTask <int>:
int result = …; return new ValueTask<int>(result);
异步终止后,它可以使用实现接口的池中的对象:
IValueTaskSource<int> vts = …; return new ValueTask<int>(vts);
Socket实现在池中支持一个这样的对象用于接收,而在传输中则支持一个对象,因为每个方向上一次都不能有多个对象等待执行。 即使在异步操作的情况下,这些重载也不会分配内存。 此行为在NetworkStream类中更加明显。
例如,在.NET Core 2.1中,Stream提供了:
public virtual ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken);
在NetworkStream中重新定义。 NetworkStream.ReadAsync方法仅使用Socket.ReceiveAsync方法,以便将Socket中的奖金广播到NetworkStream,而NetworkStream.ReadAsync也不实际分配内存。
非共享的ValueTask
当.NET Core 2.0中出现ValueTask <TResult>时,如果TResult值已经准备好,则仅在其中优化了同步执行用例,以排除Task <TResult>对象的放置。 这意味着不需要非通用的ValueTask类:对于同步执行,单例Task.CompletedTask可以简单地从方法中返回,而这是由环境在返回Task的异步方法中隐式完成的。
但是,由于在不分配内存的情况下获得异步操作,因此非共享ValueTask的使用再次变得有意义。 在.NET Core 2.1中,我们引入了通用的ValueTask和IValueTaskSource。 它们提供了通用版本的直接等效项,用于类似的用途,但返回值为空。
实施IValueTaskSource / IValueTaskSource <T>
大多数开发人员不应实现这些接口。 而且,这不是那么容易。 如果您决定这样做,.NET Core 2.1中的几种实现都可以作为起点,例如:
- AwaitableSocketAsyncEventArgs
- AsyncOperation <TResult>
- DefaultPipeReader
为了使此操作更容易,我们计划在.NET Core 3.0中引入ManualResetValueTaskSourceCore <TResult>类型中包含的所有必要逻辑,该结构可以嵌入实现IValueTaskSource <TResult>和/或IValueTaskSource的另一个对象中,以便可以将其委派给此结构是功能的大部分。 您可以从dotnet / corefx存储库中的https://github.com/dotnet/corefx/issues/32664了解更多信息。
ValueTasks应用程序模式
乍一看,ValueTask和ValueTask <TResult>的范围比Task和Task <TResult>受到更大的限制。 这是很好的,甚至可以预料的,因为使用它们的主要方法只是使用await运算符。
但是,由于它们可以包装重用的对象,因此,如果您偏离了通常的简单等待方式,则与Task和Task <TResult>相比,它们的使用受到了很大的限制。 在一般情况下,决不要对ValueTask / ValueTask <TResult>执行以下操作:
- 重复等待ValueTask / ValueTask <TResult>结果对象可能已被处置并在另一操作中使用。 相比之下,任务/任务<TResult>永远不会从完成状态转换为不完整状态,因此您可以根据需要多次对其进行重新预期,并每次都获得相同的结果。
- 并行等待ValueTask / ValueTask <TResult>结果对象期望一次仅从一个使用者使用一个回调进行处理,而尝试同时从不同的流中等待很容易导致竞争和细微的程序错误。 另外,这也是先前无效的“重新等待”操作的更具体情况。 相比之下,任务/任务<TResult>提供任意数量的并行等待。
- 在操作尚未完成时使用.GetAwaiter()。GetResult()。 IValueTaskSource / IValueTaskSource <TResult>的实现在操作完成之前不需要锁定支持,并且很可能不会这样做,因此,这样的操作肯定会导致竞速,并且可能将不会按照调用方法的预期执行。 任务/任务<TResult>阻塞调用线程,直到任务完成。
如果收到ValueTask或ValueTask <TResult>,但是需要执行这三个操作之一,则可以使用.AsTask(),获取Task / Task <TResult>,然后使用接收到的对象进行操作。 之后,您将无法再使用该ValueTask / ValueTask <TResult>。
简而言之,规则是这样的:使用ValueTask / ValueTask <TResult>时,您必须直接等待它(可能使用.ConfigureAwait(false))或调用AsTask()并不再使用它:
// , ValueTask<int> public ValueTask<int\> SomeValueTaskReturningMethodAsync(); ... // GOOD int result = await SomeValueTaskReturningMethodAsync(); // GOOD int result = await SomeValueTaskReturningMethodAsync().ConfigureAwait(false); // GOOD Task<int> t = SomeValueTaskReturningMethodAsync().AsTask(); // WARNING ValueTask<int> vt = SomeValueTaskReturningMethodAsync(); // , // // BAD: await ValueTask<int> vt = SomeValueTaskReturningMethodAsync(); int result = await vt; int result2 = await vt; // BAD: await ( ) ValueTask<int> vt = SomeValueTaskReturningMethodAsync(); Task.Run(async () => await vt); Task.Run(async () => await vt); // BAD: GetAwaiter().GetResult(), ValueTask<int> vt = SomeValueTaskReturningMethodAsync(); int result = vt.GetAwaiter().GetResult();
我希望程序员还有一种高级模式,只有经过仔细衡量并获得明显优势之后,才能应用。 ValueTask / ValueTask <TResult>类具有报告操作当前状态的多个属性,例如,如果操作完成(即,它不再运行且成功或不成功完成),则IsCompleted属性返回true,而IsCompletedSuccessfully属性仅返回true。如果它成功完成(在等待并接收结果时,它没有引发异常)。 对于最苛刻的执行线程,开发人员希望避免异步模式下产生的成本,可以在实际销毁ValueTask / ValueTask <TResult>对象的操作(例如,等待.AsTask())之前检查这些属性。 例如,在.NET Core 2.1中的SocketsHttpHandler的实现中,代码从连接读取并接收ValueTask <int>。 如果此操作是同步执行的,则不必担心操作会提前终止。 但是,如果它异步运行,我们必须挂接中断处理程序,以便中断请求断开连接。 由于这是一段非常紧张的代码,因此如果概要分析表明需要进行以下较小的更改,则可以采用以下结构:
int bytesRead; { ValueTask<int> readTask = _connection.ReadAsync(buffer); if (readTask.IsCompletedSuccessfully) { bytesRead = readTask.Result; } else { using (_connection.RegisterCancellation()) { bytesRead = await readTask; } } }
每个新的异步API方法都应该返回ValueTask / ValueTask <TResult>吗?
简短地回答:不,默认情况下仍然值得选择Task / Task <TResult>。
如上所述,Task和Task <TResult>比ValueTask和ValueTask <TResult>更易于正确使用,并且只要性能要求不超过实用性要求,则首选Task和Task <TResult>。 另外,返回ValueTask <TResult>而不是Task <TResult>的开销很小,也就是说,微基准测试显示await Task <TResult>比await ValueTask <TResult>更快。 因此,例如,如果您使用任务缓存,则您的方法将返回Task或Task,为了提高性能,值得使用Task或Task。 ValueTask / ValueTask <TResult>对象在内存中占据几个字,因此,当期望它们并且它们的字段在调用async方法的状态机中保留时,它们将在其中占据更多的内存。
- ValueTask/ValueTask<TResult> : ) , await, ) , ) , . , / .
ValueTask ValueTask<TResult>?
.NET , Task/Task<TResult>, , ValueTask/ValueTask<TResult>, , . – IAsyncEnumerator<T>, .NET Core 3.0. IEnumerator<T> MoveNext, bool, IAsyncEnumerator<T> MoveNextAsync. , , Task, . , , , ( ), await foreach, ValueTask. , . C# , , , .