C# 8 中的异步迭代器 IAsyncEnumerable 解析

时间:2024-01-28 19:42:44

异步编程已经流行很多年了,.NET 引入的 async 和 await 关键词让异步编程更具有可读性,但有一个遗憾,在 C# 8 之前都不能使用异步的方式处理数据流,直到 C# 8 引入的 IAsyncEnumerable<T> 才解决了这个问题。

说到 IAsyncEnumerable<T> ,得先说一说 IEnumerable<T> ,大家都知道,它是用同步的方式来迭代 collection 集合的,而这里的 IAsyncEnumerable<T> 则是用异步方式,换句话说: IAsyncEnumerable<T> 在迭代集合的过程中不会阻塞调用线程。

IAsyncDisposable, IAsyncEnumerable<T>, IAsyncEnumerator<T>

异步迭代器 允许我们可以用异步的方式处理数据,在这之前要了解下面三个接口:IAsyncDisposable, IAsyncEnumerable<T> 和 IAsyncEnumerator<T>,他们都是在 .NET Standard 2.1 中被引入,下面的代码片段展示了这三个接口的定义。


public interface IAsyncDisposable
{
    ValueTask DisposeAsync();
}

public interface IAsyncEnumerable<out T>
{
    IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken
    token = default);
}

public interface IAsyncEnumerator<out T> : IAsyncDisposable
{
    ValueTask<bool> MoveNextAsync();
    T Current { get; }
}

为什么要使用异步迭代器

可以想象一下你有一个数据访问层需要从数据库中一次性读取所有的数据,要想使用这个功能很简单,可以直接调用 底层提供的异步方法 XXXAsyc 实现异步调用并且一次性返回所有数据。

只要不是将所有数据都呈现在页面上的话,这种解决方案问题不是太大,很多时候更多的是通过 分页读取 的形式,其实在这方面还有一个比较好的做法就是在数据可用时立即返回给调用者。

准确的说,这里可使用 异步迭代器 的方式来解决,如果你的方法是同步返回的话,你可以使用 return yield + 返回值 IEnumerable<T> 模式,很遗憾的是,这种方式没有扩展性,因为它是需要阻塞调用线程的。

最好的解决方案就是 return yield + 返回值 IAsyncEnumerable<T> 模式,异步迭代器方法返回的是 IAsyncEnumerable<T>实例,并且可以包含一个或多个 yield return 语句。

在 C#8 中创建异步迭代器

下面的代码片段展示了一个返回 Task<IEnumerable<T>> 类型的异步方法,如下代码所示:


    class Program
    {
        const int DELAY = 1000;
        const int MIN = 1;
        const int MAX = 10;

        public static async Task Main(string[] args)
        {
            foreach (int number in await GetData())
            {
                Console.WriteLine($"{DateTime.Now}: number={number}");
            }

            Console.ReadLine();
        }

        public static async Task<IEnumerable<int>> GetData()
        {
            List<int> integers = new List<int>();
            for (int i = MIN; i <= MAX; i++)
            {
                await Task.Delay(DELAY);
                integers.Add(i);
            }
            return integers;
        }
    }

当运行上面的应用程序,它会等待 10s 之后再将所有的 1-10 的数字输出控制台上,虽然这个 GetData 是异步的,但最终还是一次性输出了,而不是一个一个的隔秒输出。

这个时候可以让 yield 关键词介入,它是在 C# 2.0 中被引入的,常用于执行状态迭代 并且按一个一个的从集合中返回数据,你不需要像上面一样创建一个集合(integers) 再返回上去,下面的代码片段是修改 GetData 方法并且合并了 yield 关键词的版本,代码如下:


static async IAsyncEnumerable<int> GetData()
{
   for (int i = MIN; i < MAX; i++)
   {
      yield return i;
      await Task.Delay(DELAY);  
   }
}

C#8 中使用异步迭代器

要想使用异步流, 需要在 foreach 前增加一个 await 关键词,如下代码所示:


        public static async Task Main(string[] args)
        {
            await foreach (int number in GetData())
            {
                Console.WriteLine($"{DateTime.Now}: number={number}");
            }

            Console.ReadLine();
        }

下面是完整的仅供参考的代码。


    class Program
    {
        const int DELAY = 1000;
        const int MIN = 1;
        const int MAX = 10;

        public static async Task Main(string[] args)
        {
            await foreach (int number in GetData())
            {
                Console.WriteLine($"{DateTime.Now}: number={number}");
            }

            Console.ReadLine();
        }

        static async IAsyncEnumerable<int> GetData()
        {
            for (int i = MIN; i < MAX; i++)
            {
                yield return i;
                await Task.Delay(DELAY);
            }
        }
    }

C# 8 中一个非常重要的特性就是支持了 IAsyncEnumerable<T>,它可以让你应用程序代码更干净,更高效 和 更高性能。

更多精彩,欢迎订阅