![](/img/trans.png)
[英]Why am I not allowed to return an IAsyncEnumerable in a method returning an 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.