簡體   English   中英

對同一個任務多次等待可能會導致阻塞

[英]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.

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