今天来写写C#中的异步迭代器 - 机制、概念和一些好用的特性

迭代器的概念

迭代器的概念在C#中出现的比较早,很多人可能已经比较熟悉了。

通常迭代器会用在一些特定的场景中。

举个例子:有一个foreach循环:

foreach (var item in Sources)
{Console.WriteLine(item);
}

这个循环实现了一个简单的功能:把Sources中的每一项在控制台中打印出来。

有时候,Sources可能会是一组完全缓存的数据,例如:List<string>

IEnumerable<string> Sources(int x)
{var list = new List<string>();for (int i = 0; i < 5; i++)list.Add($"result from Sources, x={x}, result {i}");return list;
}

这里会有一个小问题:在我们打印Sources的第一个的数据之前,要先运行完整运行Sources()方法来准备数据,在实际应用中,这可能会花费大量时间和内存。更有甚者,Sources可能是一个无边界的列表,或者不定长的开放式列表,比方一次只处理一个数据项目的队列,或者本身没有逻辑结束的队列。

这种情况,C#给出了一个很好的迭代器解决:

IEnumerable<string> Sources(int x)
{for (int i = 0; i < 5; i++)yield return $"result from Sources, x={x}, result {i}";
}

这个方式的工作原理与上一段代码很像,但有一些根本的区别 - 我们没有用缓存,而只是每次让一个元素可用。

为了帮助理解,来看看foreach在编译器中的解释:

using (var iter = Sources.GetEnumerator())
{while (iter.MoveNext()){var item = iter.Current;Console.WriteLine(item);}
}

当然,这个是省略掉很多东西后的概念解释,我们不纠结这个细节。但大体的意思是这样的:编译器对传递给foreach的表达式调用GetEnumerator(),然后用一个循环去检查是否有下一个数据(MoveNext()),在得到肯定答案后,前进并访问Current属性。而这个属性代表了前进到的元素。

上面这个例子,我们通过MoveNext()/Current方式访问了一个没有大小限制的向前的列表。我们还用到了yield迭代器这个很复杂的东西 - 至少我是这么认为的。

我们把上面的例子中的yield去掉,改写一下看看:

IEnumerable<string> Sources(int x) => new GeneratedEnumerable(x);class GeneratedEnumerable : IEnumerable<string>
{private int x;public GeneratedEnumerable(int x) => this.x = x;public IEnumerator<string> GetEnumerator() => new GeneratedEnumerator(x);IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}class GeneratedEnumerator : IEnumerator<string>
{private int x, i;public GeneratedEnumerator(int x) => this.x = x;public string Current { get; private set; }object IEnumerator.Current => Current;public void Dispose() { }public bool MoveNext(){if (i < 5){Current = $"result from Sources, x={x}, result {i}";i++;return true;}else{return false;}}void IEnumerator.Reset() => throw new NotSupportedException();
}

这样写完,对照上面的yield迭代器,理解工作过程就比较容易了:

  1. 首先,我们给出一个对象IEnumerable。注意,IEnumerableIEnumerator是不同的。

  2. 当我们调用Sources时,就创建了GeneratedEnumerable。它存储状态参数x,并公开了需要的IEnumerable方法。

  3. 后面,在需要foreach迭代数据时,会调用GetEnumerator(),而它又调用GeneratedEnumerator以充当数据上的游标。

  4. MoveNext()方法逻辑上实现了for循环,只不过,每次调用MoveNext()只执行一步。更多的数据会通过Current回传过来。另外补充一点:MoveNext()方法中的return false对应于yield break关键字,用于终止迭代。

是不是好理解了?

下面说说异步中的迭代器。

异步中的迭代器

上面的迭代,是同步的过程。而现在Dotnet开发工作更倾向于异步,使用async/await来做,特别是在提高服务器的可伸缩性方面应用特别多。

上面的代码最大的问题,在于MoveNext()。很明显,这是个同步的方法。如果它运行需要一段时间,那线程就会被阻塞。这会让代码执行过程变得不可接受。

我们能做得最接近的方法是异步获取数据:

async Task<List<string>> Sources(int x) {...}

但是,异步获取数据并不能解决数据缓存延迟的问题。

好在,C#为此特意增加了对异步迭代器的支持:

public interface IAsyncEnumerable<out T>
{IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default);
}
public interface IAsyncEnumerator<out T> : IAsyncDisposable
{T Current { get; }ValueTask<bool> MoveNextAsync();
}
public interface IAsyncDisposable
{ValueTask DisposeAsync();
}

注意,从.NET Standard 2.1.NET Core 3.0开始,异步迭代器已经包含在框架中了。而在早期版本中,需要手动引入:

# dotnet add package Microsoft.Bcl.AsyncInterfaces

目前这个包的版本号是5.0.0。

还是上面例子的逻辑:

IAsyncEnumerable<string> Source(int x) => throw new NotImplementedException();

看看foreach可以await后的样子:

await foreach (var item in Sources)
{Console.WriteLine(item);
}

编译器会将它解释为:

await using (var iter = Sources.GetAsyncEnumerator())
{while (await iter.MoveNextAsync()){var item = iter.Current;Console.WriteLine(item);}
}

这儿有个新东西:await using。与using用法相同,但释放时会调用DisposeAsync,而不是Dispose,包括回收清理也是异步的。

这段代码其实跟前边的同步版本非常相似,只是增加了await。但是,编译器会分解并重写异步状态机,它就变成异步的了。原理不细说了,不是本文关注的内容。

那么,带有yield的迭代器如何异步呢?看代码:

async IAsyncEnumerable<string> Sources(int x)
{for (int i = 0; i < 5; i++){await Task.Delay(100); // 这儿模拟异步延迟yield return $"result from Sources, x={x}, result {i}";}
}

嗯,看着就舒服。

这就完了?图样图森破。异步有一个很重要的特性:取消。

那么,怎么取消异步迭代?

异步迭代的取消

异步方法通过CancellationToken来支持取消。异步迭代也不例外。看看上面IAsyncEnumerator<T>的定义,取消标志也被传递到了GetAsyncEnumerator()方法中。

那么,如果是手工循环呢?我们可以这样写:

await foreach (var item in Sources.WithCancellation(cancellationToken).ConfigureAwait(false))
{Console.WriteLine(item);
}

这个写法等同于:

var iter = Sources.GetAsyncEnumerator(cancellationToken);
await using (iter.ConfigureAwait(false))
{while (await iter.MoveNextAsync().ConfigureAwait(false)){var item = iter.Current;Console.WriteLine(item);}
}

没错,ConfigureAwait也适用于DisposeAsync()。所以最后就变成了:

await iter.DisposeAsync().ConfigureAwait(false);

异步迭代的取消捕获做完了,接下来怎么用呢?

看代码:

IAsyncEnumerable<string> Sources(int x) => new SourcesEnumerable(x);
class SourcesEnumerable : IAsyncEnumerable<string>
{private int x;public SourcesEnumerable(int x) => this.x = x;public async IAsyncEnumerator<string> GetAsyncEnumerator(CancellationToken cancellationToken = default){for (int i = 0; i < 5; i++){await Task.Delay(100, cancellationToken); // 模拟异步延迟yield return $"result from Sources, x={x}, result {i}";}}
}

如果有CancellationToken通过WithCancellation传过来,迭代器会在正确的时间被取消 - 包括异步获取数据期间(例子中的Task.Delay期间)。当然我们还可以在迭代器中任何一个位置检查IsCancellationRequested或调用ThrowIfCancellationRequested()

此外,编译器也会通过[EnumeratorCancellation]来完成这个任务,所以我们还可以这样写:

async IAsyncEnumerable<string> Sources(int x, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{for (int i = 0; i < 5; i++){await Task.Delay(100, cancellationToken); // 模拟异步延迟yield return $"result from Sources, x={x}, result {i}";}
}

这个写法与上面的代码其实是一样的,区别在于加了一个参数。

实际应用中,我们有下面几种写法上的选择:

// 不取消
await foreach (var item in Sources)// 通过WithCancellation取消
await foreach (var item in Sources.WithCancellation(cancellationToken))// 通过SourcesAsync取消
await foreach (var item in SourcesAsync(cancellationToken))// 通过SourcesAsync和WithCancellation取消
await foreach (var item in SourcesAsync(cancellationToken).WithCancellation(cancellationToken))// 通过不同的Token取消
await foreach (var item in SourcesAsync(tokenA).WithCancellation(tokenB))

几种方式区别于应用场景,实质上没有区别。对两个Token的方式,任何一个Token被取消时,任务会被取消。

总结

同步迭代其实在各个代码中用的都比较多,但异步迭代用得很好。一方面,这是个相对新的东西,另一方面,是会有点绕,所以很多人都不敢碰。

今天这个,也是个人的一些经验总结,希望对大家理解迭代能有所帮助。

喜欢就来个三连,让更多人因你而受益

一文说通C#中的异步迭代器相关推荐

  1. 一文说通C#中的异步编程补遗

    前文写了关于C#中的异步编程.后台有无数人在讨论,很多人把异步和多线程混了. 文章在这儿:一文说通C#中的异步编程 所以,本文从体系的角度,再写一下这个异步编程.   一.C#中的异步编程演变 1. ...

  2. 一文说通C#中的异步编程

    天天写,不一定就明白. 又及,前两天看了一个关于同步方法中调用异步方法的文章,里面有些概念不太正确,所以整理了这个文章.   一.同步和异步. 先说同步. 同步概念大家都很熟悉.在异步概念出来之前,我 ...

  3. es6 async函数的异步迭代器

    async函数的异步迭代器 <迭代器>一章说过,Iterator 接口是一种数据遍历的协议,只要调用迭代器对象的next方法,就会得到一个对象,表示当前遍历指针所在的那个位置的信息.nex ...

  4. 一文说通异步 LINQ

    用不好异步 LINQ,基本上就等于用不好 LINQ 了. LINQ 这个东西,出来很早了,写过几年代码的兄弟们,或多或少都用过一些. 早期的 LINQ,主要是同步的,直到 C# 8.0 加入 IAsy ...

  5. spring mvc 异步_DeferredResult – Spring MVC中的异步处理

    spring mvc 异步 DeferredResult是一个可能尚未完成的计算的容器,它将在将来提供. Spring MVC使用它来表示异步计算,并利用Servlet 3.0 AsyncContex ...

  6. DeferredResult – Spring MVC中的异步处理

    DeferredResult是一个可能尚未完成的计算的容器,它将在将来提供. Spring MVC使用它来表示异步计算,并利用Servlet 3.0 AsyncContext异步请求处理. 简要介绍一 ...

  7. python 异步io_Python中的异步IO:完整的演练

    python 异步io Async IO is a concurrent programming design that has received dedicated support in Pytho ...

  8. JavaScript 异步迭代器

    JavaScript 异步迭代器是一种特殊的对象,它允许你在异步地迭代一组值.异步迭代器的使用方法类似于同步迭代器,但是它使用了 for await...of 语句来进行迭代. 下面是一个使用异步迭代 ...

  9. javascript迭代器_JavaScript符号,迭代器,生成器,异步/等待和异步迭代器-全部简单解释...

    javascript迭代器 by rajaraodv 通过rajaraodv JavaScript符号,迭代器,生成器,异步/等待和异步迭代器-全部简单解释 (JavaScript Symbols, ...

最新文章

  1. 小程序云开发 一开通云开发,给数据库添加一条记录
  2. linux下A免密码登录B
  3. python 渗透框架_Python渗透测试框架:PytheM
  4. mac 使用远程连接
  5. Google Shopping Feed 数据整理之XML格式实现方法
  6. android中返回刷新,Android intent 传递对象以及返回刷新
  7. Unreal、CryEngine、Gamebryo引擎介绍
  8. 跨域问题深入理解以及解决办法
  9. 南邮数据库系统设计期中测试题库(雨课堂 + 慕课)
  10. c语言中缀表达式求值_[源码和文档分享]基于C++的表达式计算求值
  11. VCLSkin皮肤在MDI窗体下的问题
  12. C语言谭浩强编程错误归纳
  13. Jmeter设置之ramp-up
  14. 多卡聚合设备基于融合系统指挥平台的解决方案
  15. 【心理咨询师考试笔记】基础理论(六)——心理咨询概论
  16. 计算机设备和打印机打不开,Windows7设备和打印机窗口打不开如何解决
  17. Z律师:创业项目如何玩转股权众筹?
  18. maglev hash算法
  19. bootstrap之入门教程
  20. docker搭建webug4.0漏洞靶场

热门文章

  1. 【codevs1230】元素查找
  2. Ubuntu16.04 + caffe-ssd + [CPU_ONLY] + KITTI 训练总结
  3. 【mongoDB运维篇③】replication set复制集
  4. android之在view中内嵌浏览器的方法
  5. Processing编译android的apk应用
  6. [导入]Asp.net中动态在中加入Scrpit标签
  7. android 设备占用_如何查看正在占用Android设备的空间
  8. facebook 分享页面_Facebook个人资料,页面和组之间有什么区别?
  9. viper4android 生效,另一种让V4a音效在Poweramp上生效的方法
  10. 香港连续25年被评为全球最自由经济体