AsyncEnumerable 随笔

AsyncEnumerable 主要是伴随着对异步迭代器的需求而产生的。之前在编写WikiClientLibrary的时候,遇到了一个和分页相关的问题。比如我们要从维基服务器获取所有页面的列表。一个最简单、使用异步的想法如下所示

这样,客户程序可以使用循环或者LINQ来使用这些条目序列。

但情况没有这么简单。服务器在一次请求中最多只能返回500条结果,那么对于大部分的维基而言,我们可能需要多次请求才能获取到所有的结果。那么,我们要怎么处理这种情况呢?

一个最直接的想法如下所示

是的,这样整个函数还是异步的,但如果用户仅仅对前10条记录感兴趣呢?比如……

看起来,问题得到了解决。

但这不是延迟调用(Lazy evaluation)。

使用上面的这种实现方法,我们会将整个列表中的所有条目全部加载,然后返回。例如,如果整个维基站点有10,000个页面条目,假设我们每次请求会得到500个条目,那么在FetchAllPagesAsync中需要20次请求才能返回结果,而其中后19次请求的结果实际上会被丢弃。

也许我们可以使用迭代器。假设我们采用阻塞调用来向服务器请求数据:

如此一来,如果客户端仅仅使用了返回的序列中的前20项,那么此迭代器就不会向服务器请求更多的结果。

但这是阻塞调用。

我们之所以要使用TPL,是因为在等待服务器返回结果的这段时间中,我们完全可以去做其他的事情。但在上面的实现中,SendRequest是阻塞函数。这不是我们想要的。

也许我们可以这样

但我实在想不出来可以怎么写……

也许我们需要一个异步迭代器、异步的 IEnumerable

问题的提出

以上的尴尬局面归结到现有的 IEnumerator接口,具体来说, IEnumerator.MoveNext是同步的。

注: Reset的存在感几乎为0。此方法仅为COM兼容性而设置。目前主流的实现方法是直接扔一个 NotSupportedException

显然,对于我们这里的情况,在调用 MoveNext时,我们需要产生并设置 Current为序列的下一个元素。对于异步调用,我们期望能出现类似于 Task<bool> MoveNextAsync(); 这样的函数,来异步地产生下一个元素。基于这样的想法,我们有了下面的接口

是的,这就是我们期盼中的异步迭代器。与之配套的是可以返回IAsyncEnumerator的IAsyncEnumerable

假设我们已经实现了一个函数 IAsyncEnumerable<Page> FetchAllPagesAsync();,那么,也许我们可以这样来使用

注意到我们拿到的是一个异步迭代器,在向此迭代器请求第一个元素之前,是不会有任何请求发生的。因为此时IAsyncEnumerator还没有被创建,更不用说调用IAsyncEnumerator.MoveNext了。因此,第一行代码肯定是同步返回的。

事情在第二行变得有意思了。我们假定库为IAsyncEnumerable也实现了一套LINQ扩展方法,就像System.Linq.Enumerable一样,那么与之对应的 TakeCount函数的声明应当如下所示

别忘了迭代器的延迟计算特性。直到 Count被调用之前,实际的计算都不会发生。而此处的 Count函数是异步的。这也很好理解,因为从迭代器中产生序列的过程是异步的,因此计算汇总过程也肯定是异步的。而且因为在前面的代码中,我们仅提取序列中的前10项,如果IAsyncEnumerator的实现得当的话,那么后面的结果是不会被产生的,回到最初的案例,也就不会出现后19次的网络请求了。

实际上,异步迭代器已经有现成的库可用了,那就是Ix-Async,或者System.Interactive.Async。使用Visual Studio的同学可以使用NuGet进行下载。

[TBC]

发布者

CXuesong

CXuesong

给我一点点考虑的时间。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*