中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

C#中的異步迭代器是什么

發布時間:2020-12-16 14:18:15 來源:億速云 閱讀:187 作者:Leah 欄目:開發技術

這篇文章給大家介紹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迭代器,理解工作過程就比較容易了:

首先,我們給出一個對象IEnumerable。注意,IEnumerable和IEnumerator是不同的。

當我們調用Sources時,就創建了GeneratedEnumerable。它存儲狀態參數x,并公開了需要的IEnumerable方法。

后面,在需要foreach迭代數據時,會調用GetEnumerator(),而它又調用GeneratedEnumerator以充當數據上的游標。

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#中的異步迭代器是什么就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

云林县| 铜陵市| 林周县| 通榆县| 无棣县| 绍兴市| 织金县| 双牌县| 元氏县| 雅安市| 河间市| 阆中市| 临洮县| 瑞金市| 广汉市| 长治市| 都匀市| 高平市| 革吉县| 三河市| 九台市| 松潘县| 波密县| 镇巴县| 田阳县| 内黄县| 乌拉特中旗| 通辽市| 临猗县| 仪陇县| 休宁县| 东乡族自治县| 湘西| 宜昌市| 长治县| 拉萨市| 齐河县| 顺平县| 新乡市| 肃南| 榆树市|