对同一个任务进行多次等待可能会导致阻塞

Dmi*_*uev 6 c# deadlock async-await

在同一个任务上使用多个等待应该小心。我在尝试使用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
    }

}
Run Code Online (Sandbox Code Playgroud)

我认为这里的问题是任务的延续将因此在一个线程上执行订阅者。如果一个等待者执行阻塞操作(例如从 中产生BlockingCollection.GetConsumingEnumerable()),它将阻塞其他等待者并且他们无法继续他们的工作。ContinueWith()我认为一个可能的解决方案是在等待任务之前调用。它将把延续分成两部分,并且阻塞操作将在新线程上执行。

有人可以确认或反驳多次等待任务的可能性吗?如果这种情况很常见,那么绕过阻塞的正确方法是什么?

Evk*_*Evk 3

考虑以下代码:

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);
    }
Run Code Online (Sandbox Code Playgroud)

如果运行它,您将看到以下输出:

1: 9, thread pool: False
2: 6, thread pool: True
3: 6, thread pool: True
4: 6, thread pool: True
Run Code Online (Sandbox Code Playgroud)

您可以在此处看到,如果没有 SynchonizationContext (或者您不使用ConfigureAwait)并且等待完成后它已经在线程池线程上运行,则它不会更改线程以继续。这正是您的代码中发生的情况:在 FirstAwaiter 和 SecondAwaiter 中完成“await t”语句后,在两种情况下,延续都在同一线程上运行,因为它是运行 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();
        });
    }
}
Run Code Online (Sandbox Code Playgroud)