1. 程式人生 > 其它 >如何在 C# 8 中使用 非同步流

如何在 C# 8 中使用 非同步流

非同步程式設計已經流行很多年了,.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>,它可以讓你應用程式程式碼更乾淨,更高效 和 更高效能。

譯文連結:https://www.infoworld.com/article/3531251/how-to-use-asynchronous-streams-in-csharp-80.html

更多高質量乾貨:參見我的 GitHub: csharptranslate