前置知识背景在C#中的async/await一文中有介绍。现在讨论一下ValueTask和Task的区别和使用方法,本文是对于Stephen Toub写的文章Understanding the Whys, Whats, and Whens of ValueTask的读后总结。
ValueTask常用的使用场景
首先我们应该都知道,Task是一个class,ValueTask是一个struct。首先但就引用类型和值类型的区别,我们知道值类型是分配在栈上的,引用类型是分配在堆上的,由GC回收,所以通常来说能用值类型就用值类型,内存使用效率更高。基于这一点,我们可以看下面这个ByteStream类的ReadByteAsync()方法。
public sealed class ByteStream : IDisposable
{
private readonly Stream stream;
private readonly byte[] buffer;
private int position;
private int bufferedBytes;
public ByteStream(Stream stream)
{
this.stream = stream;
buffer = new byte[1024 * 8];
}
/// <summary>
/// 由于大多数情况都是直接返回,不会执行await,所以用ValueTask这个值类型会更好
/// </summary>
/// <returns></returns>
public async ValueTask<byte?> ReadByteAsync()
{
if(position == bufferedBytes)
{
position = 0;
bufferedBytes = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
}
if(bufferedBytes== 0)
{
return null;
}
return buffer[position++];
}
public void Dispose() { }
}
ValueTask <TResult>何时不得使用
- 不能在同一个线程多次await;
- 不能并发await;
- 不能在操作未完成时使用GetAwaiter().GetResult();
关于第三点的解释,在Understanding the Whys, Whats, and Whens of ValueTask中,是这样的:
The IValueTaskSource / IValueTaskSource<TResult> implementation need not support
blocking until the operation completes, and likely doesn’t, so such an operation is
inherently a race condition and is unlikely to behave the way the caller intends.
这里的IValueTaskSource/IValueTaskSource<TResult>是用于实现ValueTask池化的接口,当await Task时,Task未完成,此时会在堆中分配一个Task来执行continuation续延,但是没人知道这个Task完成操作后是否还可以被重用,因为有可能之前的调用方还会对该Task做一些操作。如果可以被重用的话,那么异步架构会缓存这个对象,操作未完成的时候不可以在多个进行中的异步操作中使用同一个对象,可以在操作完成之后重用。对于ValueTask的重用对象机制,就有IValueTaskSource这个接口。所以这就是为什么不能再操作未完成时使用GetAwaiter().GetResult(),其实是跟IValueTaskSource有关系;
ValueTask一定比Task性能更好吗?
是的,但是使用方法限制较多,还不能并发使用,如果对自己的掌握程度有自信,可以一直使用ValueTask。有如下情况你应该使用ValueTask:
a) 你期望你的API的消费者直接await它们,比await Task更好的一点是,ValueTask是值类型,值类型会快速回收,不像引用类型可能呆在GC里很长时间;
b) 如果你的API对内存开销很敏感,使用ValueTask;
c) 你要么期望同步完成是非常普遍的情况,此时await直接返回,要么你能够有效地为异步完成使用对象池,因为Task的池化是异步架构自己做的,ValueTask的重用需要自己处理,除非你用IValueTaskSource接口。这一点是比较难的,你要知道如何在异步完成的时候在对象池中重用对象。
总结
这里只是简单讲了ValueTask的使用方法,错误用法,更深层的原因得看IL源码了,这个暂时搁置,只知道它不能多次await、不能并发await就够用了。.GetAwaiter().GetResult()这个部分就暂时别管。