C# 7编程模式与实践(6)

真正的解决方案是创建并使用基于结构体的Task类,而不是使用在堆上分配的Task类。实际上是使用ValueTask<T>名称创建类,并在System.Threading.Tasks.Extensions库中发布。await已对所有暴露了正确方法的类工作了,因此当前可以调用它。

手工暴露ValueTask<T>

如果预期结果在大部分时间中是同步时,并且开发人员想要去除无必要的内存分配,这正是ValueTask<T>的一个基本用例。一开始,我们假定有一个基于Task类的传统异步方法:

public async Task<Customer> ReadFromDBAsync(string key)

我们使用一个缓存方法包裹(Wrap)该方法:

public ValueTask<Customer> ReadFromCacheAsync(string key) { Customer result; if (_Cache.TryGetValue(key, out result)) return new ValueTask<Customer>(result); //没有分配no allocation else return new ValueTask<Customer>(ReadFromCacheAsync_Inner(key)); }

然后添加一个Helper方法,构建异步状态机。

async Task<Customer> ReadFromCacheAsync_Inner(string key) { var result = await ReadFromDBAsync(key); _Cache[key] = result; return result; }

完成上述代码后,调用者就可以使用与ReadFromDBAsync相同的语法去调用ReadFromCacheAsync:

async Task Test() { var a = await ReadFromCacheAsync("aaa"); var b = await ReadFromCacheAsync("bbb"); } 通用异步(Generalized Async)

上面的编程模式虽然并不难理解,但是实现起来却十分冗长。我们知道,代码编写得越冗长,越易于包含简单的错误。因此在C# 7的当前提议中,提供了通用异步返回(Generalized Async Return)。

根据当前的设计,只能对返回Task、Task<T>或void的函数使用async关键字。在提议实现后,通用异步返回将会扩展该能力到任何“类似于Task”的类上。我们这里所说的“类似于Task”,是指任何具有AsyncBuilder属性的类。这表明Helper类一直用于创建“类似于Task”的对象。

根据特性设计记录,Microsoft估计可能将会有五个人实际创建“类似于Task”的类,这些类将会被广泛接受。其余的人更有可能是去使用这五个类中的一个。下面给出对前面的例子应用新语法后的代码:

public async ValueTask<Customer> ReadFromCacheAsync(string key) { Customer result; if (_Cache.TryGetValue(key, out result)) { return result; //没有做分配。 } else { result = await ReadFromDBAsync(key); _Cache[key] = result; return result; } }

正如你所看到的,我们消除了Helper方法。新的实现看上与其它的异步方法一样,只是没有返回类型。

何时使用ValueTask<T>

可以使用ValueTask<T>替代Task<T>吗?这没有必要。解释原因稍有难度,所以我们直接引用了文档:

如果方法很有可能会同步地给出操作结果,或是由于方法每次调用时都要分配一个新的Task<TResult>以至于被频繁调用时的开销过高,这时方法可返回该值类型的一个实例。

使用ValueTask<TResult>替代Task<TResult>时存在着权衡。例如,虽然在成功地同步返回结果的情况下,ValueTask<TResult>会少做一次内存分配,但是ValueTask<TResult>还是包括两个字段,其中作为引用类型的Task<TResult>构成一个字段。这意味着在方法调用结束时会返回两个字段的数据,而不是一个字段,即需要拷贝更多的数据。这同样意味着如果在async方法中有一个只返回其中一个字段的方法在等待状态,那么该async方法的状态机将会增大,因为这时需要被存储的结构体具有两个字段,而不是一个引用。

更进一步,如果使用中不只是需要通过await消费异步操作的结果,那么ValueTask<TResul>会产生更错综复杂的编程模型,进而导致事实上分配了更多的内存。例如,假定有一个方法返回一个使用被缓存的Task作为通用结果的Task<TResult>,或是返回一个ValueTask<TResult>。当消费者想将返回结果作为Task<TResult>使用,正如在Task.WhenAll和Task.WhenAny方法中的用法,那么首先需要调用ValueTask<TResult>.AsTask将ValueTask<TResult>转化为Task<TResult>。但是调用ValueTask<TResult>.AsTask会导致一次内存分配,这在一开始就使用缓存的Task<TResult>的情况下是本可以避免的。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/82947a4b5f7533883a344c52de3e78ac.html