簡體   English   中英

在 function 中迭代 IAsyncEnumerable 並返回 IAsyncEnumerable 並取消

[英]Iterating an IAsyncEnumerable in a function returning an IAsyncEnumerable with cancellation

正如標題所說,我必須關注 function:

public async IAsyncEnumerable<Job> GetByPipeline(int pipelineId,
    [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
    await foreach (var job in context.Jobs.Where(job => job.Pipeline.Id == pipelineId)
        .AsAsyncEnumerable()
        .WithCancellation(cancellationToken)
        .ConfigureAwait(false))
    {
        yield return job;
    }
}

我很難理解取消令牌的去向,並且感覺我在太多地方使用它。

當你解構所有花哨的異步東西時,這里實際發生了什么? 還有沒有更好的方法來編寫這個 function?

對於初學者,此方法可以簡化為:

public IAsyncEnumerable<Job> GetByPipeline(int pipelineId)
{
    return context.Jobs
                  .Where(job => job.Pipeline.Id == pipelineId)
                  .AsAsyncEnumerable();
}

甚至

public IAsyncEnumerable<Job> GetByPipeline(int pipelineId)
    => context.Jobs
              .Where(job => job.Pipeline.Id == pipelineId)
              .AsAsyncEnumerable();

該方法對job沒有任何作用,因此不需要對其進行迭代。

消除

如果該方法實際使用了job ,應該在哪里使用取消令牌?

讓我們稍微清理一下方法。 等效的是:

public async IAsyncEnumerable<Job> GetByPipeline(
      int pipelineId, 
      [EnumeratorCancellation] CancellationToken ct = default)
{
    //Just a query, doesn't execute anything
    var query =context.Jobs.Where(job => job.Pipeline.Id == pipelineId);

    //Executes the query and returns the *results* as soon as they arrive in an async stream
    var jobStream=query.AsAsyncEnumerable();

    //Process the results from the async stream as they arrive
    await foreach (var job in jobStream.WithCancellation(ct).ConfigureAwait(false))
    {
        //Does *that* need cancelling?
        DoSometingExpensive(job);
    }
}

IQueryable query不運行任何東西,它代表查詢。 它不需要取消。

AsAsyncEnumerable()AsEnumerable()ToList()執行查詢並返回一些結果。 ToList()等消耗所有結果,而As...Enumerable()方法僅在請求時產生結果。 查詢不能被取消, As_Enumerable()方法不會返回任何東西,除非被要求,所以它們不需要取消。

await foreach將遍歷整個異步 stream 所以如果我們希望能夠中止它,我們確實需要傳遞取消令牌。

最后, DoSometingExpensive(job); 需要取消嗎? 如果花費太長時間,我們是否希望能夠擺脫它? 或者我們可以等到它完成后再退出循環嗎? 如果它需要取消,它也需要 CancellationToken。

配置等待

最后, ConfigureAwait(false)不參與取消,並且可能根本不需要。 沒有它,每次await執行后都會返回到原始同步上下文。 在桌面應用程序中,這意味着 UI 線程。 這就是允許我們在異步事件處理程序中修改 UI 的原因。

如果GetByPipeline在桌面應用程序上運行並想要修改 UI,則必須刪除ConfugureAwait

await foreach (var job in jobStream.WithCancellation(ct))
{
        //Update the UI
        toolStripProgressBar.Increment(1);
        toolStripStatusLabel.Text=job.Name;
        //Do the actual job
        DoSometingExpensive(job);
}

使用ConfigureAwait(false) ,在線程池線程上繼續執行,我們無法觸摸 UI。

庫代碼不應影響執行的恢復方式,因此大多數庫使用ConfigureAwait(false)並將最終決定權留給 UI 開發人員。

如果GetByPipeline是一個庫方法,請使用ConfigureAwait(false)

想象一下,在 Entity Framework 深處的某個地方是GetJobs方法,它從數據庫中檢索Job對象:

private static async IAsyncEnumerable<Job> GetJobs(DbDataReader dataReader,
    [EnumeratorCancellation]CancellationToken cancellationToken = default)
{
    while (await dataReader.ReadAsync(cancellationToken))
    {
        yield return new Job()
        {
            Id = (int)dataReader["Id"],
            Data = (byte[])dataReader["Data"]
        };
    }
}

現在假設Data屬性包含一個巨大的字節數組,其中包含與Job相關的數據。 檢索每個Job的數組可能需要一些不平凡的時間。 在這種情況下,打破迭代之間的循環是不夠的,因為在調用Cancel方法和引發OperationCanceledException之間會有明顯的延遲。 這就是方法DbDataReader.ReadAsync需要CancellationToken的原因,以便可以立即取消查詢。

現在的挑戰是如何將客戶端代碼傳遞的CancellationToken傳遞給GetJobs方法,當諸如context.Jobs之類的屬性在路上時。 解決方案是WithCancellation擴展方法,它存儲令牌並將其更深地傳遞給接受用EnumeratorCancellation屬性修飾的參數的方法。

因此,在您的情況下,您已正確完成所有操作。 您在IAsyncEnumerable返回方法中包含了一個cancellationToken參數,這是推薦的做法。 這樣就不會浪費鏈接到您的GetByPipeline方法的后續WithCancellation 然后,您在方法中的WithCancellation之后將AsAsyncEnumerable鏈接起來,這也是正確的。 否則CancellationToken將無法到達其最終目的地GetJobs方法。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM