ValueTask -为什么,为什么以及如何?

翻译序言


与科学文章不同,这类文章很难“接近文本”地翻译,因此必须进行大量改编。 因此,对于处理原始文章的文字方面的某些自由,我深表歉意。 我的目标只有一个-使翻译易于理解,即使在某些地方它与原始文章有很大出入。 对于建设性的批评以及对译文的修正/补充,我将不胜感激。


引言


System.Threading.Tasks命名空间和Task类首先是在.NET Framework 4中引入的。此后,此类型及其派生类Task<TResult>牢固地进入了.NET编程的实践,并成为异步模型的关键方面。在C#5中实现,其async/await 。 在本文中,我将讨论引入新类型的ValueTask/ValueTask<TResult> ,其目的是在处理内存的开销起关键作用的情况下提高异步代码的性能。



工作任务


Task多种用途,但主要目的是“承诺”-表示等待操作完成的能力的对象。 您启动操作并获取Task 。 操作本身完成后,将完成此Task 。 在这种情况下,有三个选项:


  1. 该操作在启动器线程中同步完成。 例如,当访问一些已经在buffer中的数据时
  2. 该操作是异步执行的,但是设法发起方收到Task例如,当执行对尚未缓冲的数据的快速访问时
  3. 该操作是异步执行的,并启动程序接收到Task 之后结束 例如通过网络接收数据

为了获得异步调用的结果,客户端可以在等待完成时阻塞调用线程,这通常与异步的概念相抵触,或者提供一种将在异步操作完成后执行的回调方法。 使用Task类的对象的ContinueWith方法显式呈现.NET 4中的回调模型,该方法接收一个委托,该委托在异步操作完成时被调用。


 SomeOperationAsync().ContinueWith(task => { try { TResult result = task.Result; UseResult(result); } catch (Exception e) { HandleException(e); } }); 

使用.NET Frmaework 4.5和C#5,通过引入async/await关键字及其背后的机制,简化了异步操作的结果。 这种机制(生成的代码)能够优化上述所有情况,即使到达目标路径也能正确处理完成。


 TResult result = await SomeOperationAsync(); UseResult(result); 

Task类非常灵活,并具有多个优点。 例如,您可以多次“期望”此类的对象,可以由任意数量的消费者竞争性地预期结果。 可以将一个类的实例存储在字典中,以用于任何数量的后续调用,其目标是将来“等待”。 所描述的方案使您可以将Task对象视为一种异步获取结果的缓存。 此外,如果脚本需要, Task还可以阻止等待线程,直到操作完成。 也有所谓的。 组合器,用于等待任务集完成的各种策略,例如“ Task.WhenAny”-异步等待许多任务中第一个任务的完成。


但是,尽管如此,最常见的用例只是启动异步操作,然后等待其执行结果。 这种简单的情况非常普遍,不需要上面的灵活性:


 TResult result = await SomeOperationAsync(); UseResult(result); 

这与我们编写同步代码的方式非常相似(例如TResult result = SomeOperation(); )。 此选项自然转换为async/await


此外,尽管具有所有优点,但Task类型具有潜在的缺陷。 Task是一个类,这意味着每个创建任务实例的操作都会在堆上分配一个对象。 我们创建的对象越多,GC所需的工作就越多,垃圾回收器的工作就花费了更多的资源,这些资源可用于其他目的。 这对于代码来说是一个明显的问题,其中一方面经常创建Task实例,另一方面又增加了对吞吐量和性能的要求。


在许多情况下,运行时库和主库可以减轻这种影响。 例如,如果您编写以下方法:


 public async Task WriteAsync(byte value) { if (_bufferedCount == _buffer.Length) { await FlushAsync(); } _buffer[_bufferedCount++] = value; } 

并且通常,缓冲区中将有足够的空间,操作将同步结束。 如果是这样,则返回的任务没有什么特别的,没有返回值,并且该操作已经完成。 换句话说,我们正在处理Task ,它等效于同步void操作。 在这种情况下,运行时仅缓存Task对象,并且每次将其用作任何async Task的结果-同步完成的方法( Task.ComletedTask )。 再举一个例子,假设您编写:


 public async Task<bool> MoveNextAsync() { if (_bufferedCount == 0) { await FillBuffer(); } return _bufferedCount > 0; } 

以同样的方式,假设大多数情况下缓冲区中有一些数据。 该方法检查_bufferedCount ,查看该变量大于零,然后返回true 。 仅在验证时未缓冲数据时,才需要异步操作。 尽管如此,只有两个可能的逻辑结果( truefalse ),以及通过Task<bool>两个可能的返回状态。 基于同步完成或异步,但在退出方法之前,运行时将缓存Task<bool>两个实例(一个表示true ,另一个表示false ),并返回所需的一个,从而避免了其他分配。 当您必须创建一个新的Task<bool>对象时,唯一的选择是异步执行,这种情况在“返回”之后结束。 在这种情况下,该方法必须创建一个新的Task<bool>对象,因为 在退出该方法时,尚不知道操作完成的结果。 返回的对象必须是唯一的,因为 它将最终存储异步操作的结果。


从运行时还有其他类似的缓存示例。 但是,这种策略并非在所有地方都适用。 例如,方法:


 public async Task<int> ReadNextByteAsync() { if (_bufferedCount == 0) { await FillBuffer(); } if (_bufferedCount == 0) { return -1; } _bufferedCount--; return _buffer[_position++]; } 

也经常同步结束。 但是,与前面的示例不同,此方法返回的整数结果大约有40亿个可能值。 要缓存Task<int> ,在这种情况下,将需要数百GB的内存。 此处的环境还支持Task<int>的小型缓存,其中包含几个较小的值。 因此,例如,如果操作同步完成(缓冲区中存在数据),结果为4,则将使用缓存。 但是,如果结果是同步的,但完成是42,则将创建一个新的Task<int>对象,类似于调用Task.FromResult(42)


许多库实现都尝试通过支持自己的缓存来缓解这些情况。 一个示例是MemoryStream.ReadAsync的重载。 .NET Framework 4.5中引入的此操作始终同步结束,因为 这只是从内存中读取。 ReadAsync返回一个Task<int> ,其中整数结果表示读取的字节数。 在代码中,经常在循环中使用ReadAsync时发生这种情况。 此外,是否存在以下症状:


  • 在循环的大多数迭代中,请求的字节数不会改变。
  • 在大多数迭代中, ReadAsync可以读取请求的字节数。

也就是说,对于重复调用, ReadAsync同步运行并返回Task<int>对象, ReadAsync迭代的结果相同。 从逻辑MemoryStreamMemoryStream缓存上一个成功完成的任务,对于所有后续调用,如果新结果与上一个匹配,则从缓存中返回一个实例。 如果结果不匹配,则使用Task.FromResult创建一个新实例,该实例又将在返回之前进行缓存。


但是,尽管如此,在许多情况下,即使同步完成,也必须强制操作创建新的Task<TResult>对象。


ValueTask <TResult>和同步完成


最终,所有这些都是将新型ValueTask<TResult>引入.NET Core 2.0的动机。 同样,通过nuget包System.Threading.Tasks.Extensions ,其他.NET版本中也提供了此类型。


.NET Core 2.0中引入ValueTask<TResult>作为能够包装TResultTask<TResult> 。 这意味着可以从async方法返回此类型的对象。 这种类型的引入的第一个ValueTask<TResult>是立即可见的:如果该方法成功且同步完成,则无需在堆上创建任何内容,只需创建一个带有结果值的ValueTask<TResult>实例ValueTask<TResult> 。 仅当方法异步退出时,我们才需要创建Task<TResult> 。 在这种情况下, ValueTask<TResult>用作Task<TResult>的包装。 ValueTask<TResult>优化目的,决定使ValueTask<TResult>能够聚合Task<TResult> :在成功和失败的情况下,异步方法创建Task<TResult> ,从内存优化的角度来看,最好聚合Task<TResult>对象本身Task<TResult> ,以便在ValueTask<TResult>保留其他字段以用于各种完成情况(例如,存储异常)。


鉴于以上所述,不再需要在诸如上述MemoryStream.ReadAsync方法中进行缓存,而是可以按以下方式实现:


 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(使用Memory<byte>而不是byte[]作为参数)中StreamReadAsync方法的新重载返回ValueTask<int>的实例。 这样可以大大减少使用流时的分配数量(通常, ReadAsync方法是同步完成的,例如MemoryStream的示例)。


但是,当开发具有高带宽的服务时,异步终止并不少见,我们需要尽力避免额外的分配。


如前所述,在async/await模型中,任何异步完成的操作都必须返回唯一的对象才能等待完成。 独特是因为 它将用作执行回调的渠道。 但是请注意,此构造并未说明在异步操作完成之后是否可以重用返回的等待对象。 如果可以重用对象,则API可以为这些类型的对象维护一个池。 但是,在这种情况下,此池无法支持并发访问-池中的对象将从“已完成”状态变为“未完成”状态,反之亦然。


为了支持使用此类池的可能性,在.NET Core 2.1中添加了IValueTaskSource<TResult>接口,并扩展了ValueTask<TResult>结构:现在,这种类型的对象不仅可以包装TResultTask<TResult>对象,还可以包装IValueTaskSource<TResult>实例。 新接口提供了基本功能,允许ValueTask<TResult>对象以与IValueTaskSource<TResult>相同的方式与IValueTaskSource<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/IsCompletedSuccessfully -允许您确定操作是否已完成(成功)。 在ValueTask<TResult>使用ValueTask<TResult>触发回调。 GetResult用于获取结果或引发异常。


大多数开发人员不太可能需要处理IValueTaskSource<TResult>接口,因为 异步方法返回时,将其隐藏在ValueTask<TResult>实例后面。 接口本身主要是为那些开发高性能API的人设计的,旨在避免不必要的工作。


在.NET Core 2.1中,有几种此类API的示例。 其中最著名的是Socket.ReceiveAsyncSocket.SendAsync方法的新重载。 例如:


 public ValueTask<int> ReceiveAsync( Memory<byte> buffer, SocketFlags socketFlags, CancellationToken cancellationToken = default); 

ValueTask<int>类型的对象用作返回值。
如果该方法同步退出,则它将返回带有相应值的ValueTask<int>


 int result = …; return new ValueTask<int>(result); 

如果操作异步完成,则使用一个缓存对象,该对象实现IValueTaskSource<TResult>接口:


 IValueTaskSource<int> vts = …; return new ValueTask<int>(vts); 

Socket实现支持一个缓存的对象用于接收,而一个缓存的对象用于发送数据,只要每个对象在没有竞争的情况下使用(例如,没有竞争性的数据发送)。 即使在异步执行的情况下,该策略也会减少分配的额外内存量。
描述的.NET Core 2.1中的Socket优化对NetworkStream的性能产生了积极影响。 它的重载是Stream类的ReadAsync方法:


 public virtual ValueTask<int> ReadAsync( Memory<byte> buffer, CancellationToken cancellationToken); 

只是将工作委托给Socket.ReceiveAsync方法。 就使用内存而言,提高套接字方法的效率可以提高NetworkStream方法的效率。


非通用ValueTask


早些时候,我多次提到.NET Core 2.0中ValueTask<T>的最初目标是优化具有“非空”结果的方法的同步完成情况。 这意味着不需要非类型的ValueTask :在同步完成的情况下,方法通过Task.CompletedTask属性使用单例,并且还隐式接收async Task方法的运行时。


但是,随着避免不必要分配的能力的出现以及异步执行的出现,对非类型化ValueTask再次变得重要起来。 因此,在.NET Core 2.1中,我们引入了ValueTaskIValueTaskSource 。 它们是对应泛型类型的类似物,并且以相同的方式使用,但是用于返回空( void )的方法。


实施IValueTaskSource / IValueTaskSource <T>


大多数开发人员将不需要实现这些接口。 而且要实现它们并非易事。 如果您决定需要自己实现它们,那么在.NET Core 2.1内,可以作为示例的几种实现:



为了简化这些任务( IValueTaskSource / IValueTaskSource<T> ),我们计划在.NET Core 3.0中引入类型ManualResetValueTaskSourceCore<TResult> 。 这种结构将封装所有必要的逻辑。 ManualResetValueTaskSourceCore<TResult>实例可以在实现IValueTaskSource<TResult>和/或IValueTaskSource另一个对象中使用,并将大部分工作委托给它。 您可以在ttps上了解有关此的更多信息://github.com/dotnet/corefx/issues/32664。


使用ValueTasks的正确模型


甚至粗略地检查也ValueTaskValueTask<TResult>TaskTask<TResult> ValueTask<TResult>更多的限制。 这是正常的,甚至是可取的,因为它们的主要目标是等待异步执行完成。


尤其是,由于ValueTaskValueTask<TResult>可以聚合可重用对象的事实而产生了明显的限制。 通常, 在使用 ValueTask / ValueTask<TResult> * 时,绝不要执行以下操作* 让我通过“ Never” *重新制定):


  • 切勿重复使用同一ValueTask / ValueTask<TResult>对象

动机: TaskTask<TResult>实例永远不会从“已完成”状态变为“未完成”状态,我们可以根据需要多次使用它们等待结果-完成后,我们将始终获得相同的结果。 相反,由于ValueTask / ValueTask<TResult> ,它们可以充当重用对象的包装器,这意味着它们的状态可以更改,因为 重用对象的状态根据定义发生了变化-从“完成”变为“未完成”,反之亦然。


  • <从不ValueTask / ValueTask&lt;TResult&gt; 在竞争模式下。

动机:一个包装的对象希望一次只能从一个使用者使用一个回调,并且试图竞争竞争很容易导致竞争状况和细微的编程错误。 竞争期望,这是上述多重期望中描述的选项之一。 请注意, Task / Task<TResult>允许任何数量的竞争期望。


  • 在操作完成之前,切勿使用.GetAwaiter().GetResult()

动机:在操作完成之前, IValueTaskSource / IValueTaskSource<TResult>不应支持锁定。 实际上,阻塞会导致竞争状况,这不太可能是消费者方面的预期行为。 使用Task / Task<TResult>可以执行此操作,从而阻塞调用线程,直到操作完成。


但是,如果您仍然需要执行上述操作之一,并且被调用的方法返回ValueTask / ValueTask<TResult>实例, ValueTask怎么办? 对于这种情况, ValueTask / ValueTask<TResult>提供了.AsTask()方法。 通过调用此方法,您将获得Task / Task<TResult>的实例,并且您已经可以对其执行必要的操作。 不允许在调用.AsTask()之后重用原始对象。


: ValueTask / ValueTask<TResult> , ( await ) (, .ConfigureAwait(false) ), .AsTask() , ValueTask / ValueTask<TResult> .


 // Given this ValueTask<int>-returning method… 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(); ... // storing the instance into a local makes it much more likely it'll be misused, // but it could still be ok // BAD: awaits multiple times ValueTask<int> vt = SomeValueTaskReturningMethodAsync(); int result = await vt; int result2 = await vt; // BAD: awaits concurrently (and, by definition then, multiple times) ValueTask<int> vt = SomeValueTaskReturningMethodAsync(); Task.Run(async () => await vt); Task.Run(async () => await vt); // BAD: uses GetAwaiter().GetResult() when it's not known to be done ValueTask<int> vt = SomeValueTaskReturningMethodAsync(); int result = vt.GetAwaiter().GetResult(); 

, "", , ( , ).


ValueTask / ValueTask<TResult> , . , IsCompleted true , ( , ), — false , IsCompletedSuccessfully true . " " , , , , , . await / .AsTask() .Result . , SocketsHttpHandler .NET Core 2.1, .ReadAsync , ValueTask<int> . , , , . , .. . 因为 , , , , :


 int bytesRead; { ValueTask<int> readTask = _connection.ReadAsync(buffer); if (readTask.IsCompletedSuccessfully) { bytesRead = readTask.Result; } else { using (_connection.RegisterCancellation()) { bytesRead = await readTask; } } } 

, .. ValueTask<int> , .Result , await , .


API ValueTask / ValueTask<TResult>?


, . Task / ValueTask<TResult> .


, Task / Task<TResult> . , "" / , Task / Task<TResult> . , , ValueTask<TResult> Task<TResult> : , , await Task<TResult> ValueTask<TResult> . , (, API Task Task<bool> ), , , Task ( Task<bool> ). , ValueTask / ValueTask<TResult> . , async-, ValueTask / ValueTask<TResult> , .


, ValueTask / ValueTask<TResult> , :


  1. , API ,
  2. API ,
  3. , , , .

, abstract / virtual , , / ?


接下来是什么?


.NET, API, Task / Task<TResult> . , , API c ValueTask / ValueTask<TResult> , . IAsyncEnumerator<T> , .NET Core 3.0. IEnumerator<T> MoveNext , . — IAsyncEnumerator<T> MoveNextAsync . , Task<bool> , , . , , , ( ), , , await foreach -, , MoveNextAsync , ValueTask<bool> . , , , " " , . , C# , .


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


All Articles