[英]Multiple await on same task may cause blocking
在同一個任務上使用多個等待應該小心。 我在嘗試使用BlockingCollection.GetConsumingEnumerable()
方法時遇到過這種情況。 並以這個簡化的測試結束。
class TestTwoAwaiters
{
public void Test()
{
var t = Task.Delay(1000).ContinueWith(_ => Utils.WriteLine("task complete"));
var w1 = FirstAwaiter(t);
var w2 = SecondAwaiter(t);
Task.WaitAll(w1, w2);
}
private async Task FirstAwaiter(Task t)
{
await t;
//await t.ContinueWith(_ => { });
Utils.WriteLine("first wait complete");
Task.Delay(3000).Wait(); // execute blocking operation
}
private async Task SecondAwaiter(Task t)
{
await t;
Utils.WriteLine("second wait complete");
Task.Delay(3000).Wait(); // execute blocking operation
}
}
我認為這里的問題是任務的延續將相應地在一個線程上執行訂閱者。 如果一個 awaiter 執行阻塞操作(例如從BlockingCollection.GetConsumingEnumerable()
),它將阻塞其他等待器並且他們無法繼續他們的工作。 我認為一個可能的解決方案是在等待任務之前調用ContinueWith()
。 它會將延續中斷為兩部分,並且將在新線程上執行阻塞操作。
有人可以多次確認或反駁等待任務的可能性。 如果它很常見,那么繞過阻塞的正確方法是什么?
考慮以下代碼:
private static async Task Test() {
Console.WriteLine("1: {0}, thread pool: {1}", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
await Task.Delay(1000);
Console.WriteLine("2: {0}, thread pool: {1}", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
await Task.Delay(1000);
Console.WriteLine("3: {0}, thread pool: {1}", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
await Task.Delay(1000);
Console.WriteLine("4: {0}, thread pool: {1}", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
}
如果運行它,您將看到以下輸出:
1: 9, thread pool: False
2: 6, thread pool: True
3: 6, thread pool: True
4: 6, thread pool: True
你在這里看到,如果沒有 SynchonizationContext(或者你不使用 ConfigureAwait)並且等待完成后它已經在線程池線程上運行,它不會改變線程以繼續。 這正是您的代碼中發生的情況:在 FirstAwaiter 和 SecondAwaiter 中的“await t”語句完成后,在這兩種情況下,continuation 都在同一線程上運行,因為它是 Delay(1000) 運行的線程池線程。 當然,當 FirstAwaiter 執行它的延續時,SecondAwaiter 將阻塞,因為它的延續被發布到同一個線程池線程。
編輯:如果您將使用 ContinueWith 而不是 await,您可以“修復”您的問題(但請注意對您的問題的評論):
internal class TestTwoAwaiters {
public void Test() {
Console.WriteLine("Mail thread is {0}", Thread.CurrentThread.ManagedThreadId);
var t = Task.Delay(1000).ContinueWith(_ => {
Console.WriteLine("task complete on {0}", Thread.CurrentThread.ManagedThreadId);
});
var w1 = FirstAwaiter(t);
var w2 = SecondAwaiter(t);
Task.WaitAll(w1, w2);
}
private static Task FirstAwaiter(Task t) {
Console.WriteLine("First await on {0}", Thread.CurrentThread.ManagedThreadId);
return t.ContinueWith(_ =>
{
Console.WriteLine("first wait complete on {0}", Thread.CurrentThread.ManagedThreadId);
Task.Delay(3000).Wait();
});
}
private static Task SecondAwaiter(Task t) {
Console.WriteLine("Second await on {0}", Thread.CurrentThread.ManagedThreadId);
return t.ContinueWith(_ => {
Console.WriteLine("Second wait complete on {0}", Thread.CurrentThread.ManagedThreadId);
Task.Delay(3000).Wait();
});
}
}
這里有兩種擴展方法,一種用於Task
,一種用於Task<TResult>
,確保await
之后的異步繼續。 結果和異常按預期傳播。
public static class TaskExtensions
{
/// <summary>Creates a continuation that executes asynchronously when the target
/// <see cref="Task"/> completes.</summary>
public static Task ContinueAsync(this Task task)
{
return task.ContinueWith(t => t,
default, TaskContinuationOptions.RunContinuationsAsynchronously,
TaskScheduler.Default).Unwrap();
}
/// <summary>Creates a continuation that executes asynchronously when the target
/// <see cref="Task{TResult}"/> completes.</summary>
public static Task<TResult> ContinueAsync<TResult>(this Task<TResult> task)
{
return task.ContinueWith(t => t,
default, TaskContinuationOptions.RunContinuationsAsynchronously,
TaskScheduler.Default).Unwrap();
}
}
用法示例:
await t.ContinueAsync();
更新:同步執行延續的問題行為僅影響 .NET Framework。 .NET Core 不受影響(延續在線程池線程中異步執行),因此上述解決方法僅適用於在 .NET Framework 上運行的應用程序。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.