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();
}
View Code

(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;
    }
}
View Code

对示例代码的调用及其一次结果如下:

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);
        }
    }
}
View Code

 (2)对外公开一个支持异步的迭代器

则可以使用 foreach 进行访问,foreach 前须加关键字 await

public IAsyncEnumerator<string> GetAsyncEnumerator(CancellationToken cancellationToken = default)
{
    return this;
}
View Code

 foreach 进行访问及其一次结果如下:

private static async void ToTest2_3()
{
    var test2_ = new Test2_();
    await foreach (var item in test2_)
    {
        Console.WriteLine(item);
    }
}
View Code

 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);
            }
        }
    }
}
View Code

(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);
            }
        }
    }
}
View Code

(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 的使用》中的“控制流流转”。

posted @ 2023-12-07 15:12  误会馋  阅读(390)  评论(0编辑  收藏  举报