C#中 IAsyncEnumerable 与 IAsyncEnumerator 的使用
C#中 IAsyncEnumerable 与 IAsyncEnumerator 的使用
1.支持异步的迭代器
是实现了接口 IAsyncEnumerator 的实例。它提供了一种异步方式以获取集合的下一个元素,进而允许“实现它的类或结构”可以异步地访问集合,并返回集合的元素。
接口 IAsyncEnumerable ,用于对外公开一个支持异步的枚举器。
//支持异步的枚举器 public interface IAsyncEnumerator<out T> : IAsyncDisposable { //获取枚举器当前位置的元素 T Current { get; } //导步地将枚举器前进到集合的下一个元素: //返回一个 ValueTask<bool>,通过 await运算符,可以追踪到枚举器是否成功推进到下一个元素; //如果枚举器已成功推进到下一个元素,返回 true; //如果枚举器已超过集合的末尾,返回 false。 ValueTask<bool> MoveNextAsync(); } //对外公开一个支持异步的枚举器 public interface IAsyncEnumerable<out T> { //获取支持异步的枚举器,cancellationToken: 一个 System.Threading.CancellationToken,可用于取消异步迭代。 IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default); } //提供异步释放非托管资源的机制 public interface IAsyncDisposable { //异步释放或重置非托管资源; //返回一个 ValueTask,通过 await运算符,可以追踪到“释放或重置非托管资源”任务是否完成 ValueTask DisposeAsync(); }
(1)示例代码
用于实现一个支持异步的迭代器 Test2_:假定一个耗时的工作,它随机生成一个整数,如果合法,则读取数据并返回。
/// <summary> /// 支持异步的迭代器 /// </summary> public class Test2_ : IAsyncEnumerator<string> { private readonly string[] data = { " a", " b", " c" }; public async ValueTask<bool> MoveNextAsync() { var result = await Task.Run(() => { Thread.Sleep(500); var random = new Random(); int randomInt = random.Next(0, 5); bool valid = randomInt < data.Length; return (valid ? data[randomInt] : null, valid); }); _current = result.Item1; return result.valid; } private string _current = null!; public string Current { get { return _current; } } public ValueTask DisposeAsync() { return ValueTask.CompletedTask; } }
对示例代码的调用及其一次结果如下:
internal partial class Program { static void Main(string[] args) { Task.Run(() => { ToTest2_2(); }); Console.ReadKey(); } private static async void ToTest2_2() { var test2_ = new Test2_(); while (await test2_.MoveNextAsync()) { Console.WriteLine(test2_.Current); } } }
(2)对外公开一个支持异步的迭代器
则可以使用 foreach 进行访问,foreach 前须加关键字 await。
public IAsyncEnumerator<string> GetAsyncEnumerator(CancellationToken cancellationToken = default) { return this; }
foreach 进行访问及其一次结果如下:
private static async void ToTest2_3() { var test2_ = new Test2_(); await foreach (var item in test2_) { Console.WriteLine(item); } }
2.使用关键字 yield 简化实现迭代器
(1)开发者不用再实现属性 Current,以及不用实现方法 MoveNextAsync() 和 DisposeAsync() ,这些工作都交给了编译器,由编译器自动完成。
public class Test2__ { private readonly string[] data = { " a", " b", " c" }; public async IAsyncEnumerator<string> E() { var result = await Task.Run(() => { Thread.Sleep(500); var random = new Random(); int randomInt = random.Next(0, 5); bool valid = randomInt < data.Length; return (valid ? data[randomInt] : null, valid); }); if (result.valid) { yield return result.Item1; } } public IAsyncEnumerator<string> GetAsyncEnumerator(CancellationToken cancellationToken = default) { return E(); } } internal partial class Program { private static async void ToTest2_4() { for (int i = 0; i < 100; i++) { var test2__ = new Test2__(); await foreach (var item in test2__) { Console.WriteLine(item); } } } }
(2)甚至进一步简化实现,连方法 GetAsyncEnumerator() 都不用实现,一并交给了编译器:
public class Test2__1 { private readonly string[] data = { " a", " b", " c" }; public async IAsyncEnumerable<string> Y() { var result = await Task.Run(() => { Thread.Sleep(500); var random = new Random(); int randomInt = random.Next(0, 5); bool valid = randomInt < data.Length; return (valid ? data[randomInt] : null, valid); }); if (result.valid) { yield return result.Item1; } } } internal partial class Program { private static async void ToTest2_5() { for (int i = 0; i < 100; i++) { var test2__1 = new Test2__1(); await foreach (var item in test2__1.Y()) { Console.WriteLine(item); } } } }
(3)控制流流转方式:
控制流流转方式,结合下图进行说明:
- await foreach 语句所在的方法的线程,即调用方线程,它运行至异步方法第一个 await 时终止,归还给线程池。
- Task 类 Run 方法新开一个线程(Task 线程)用于执行任务,任务执行完毕,由 Task 线程继续执行 await 之后的代码;
遇到 yield return 语句,转至执行 foreach 循环体,结束后回到异步方法中,继续执行 yield return 语句之后的代码,
跳转回 for 语句(灰色线),判断 i 是否符合条件(< 3),如果是,继续执行,直至遇到新的 await 语句。
遇到新的 await 语句时,逻辑上,线程终止,归还给线程池。
新的 await 语句的 Task 类 Run 方法又新开一个线程,重复上一步步骤【实际上可能没有新创建线程,而是复用旧线程,以避免创建线程的开销,但在逻辑上可以视为Task 类 Run 方法又新开了一个线程】。 - 新线程继续重复执行,至 for 语句时,判断 i 是否符合条件(< 3),如果否,跳出 for 循环,Task 线程执行 for 语句之后的代码直到异步方法结束(蓝色线);
异步方法结束,接着不执行 foreach 循环体,而是执行其后代码,直到调用方法方法结束。
总结:
- foreach 语句遇到异步方法的第一个 await 时结束;
- await 语句 Task 新开线程,执行之后的所有代码(包括 foreach 之后的代码),除非遇到新的await。
相关的控制流流转,可以参考:《C#中关键字 yield 的使用》 中的“执行顺序”,和 《C#中关键字 async 和 await 的使用》中的“控制流流转”。