C# 同一任务上的多次等待可能会导致阻塞

C# 同一任务上的多次等待可能会导致阻塞,c#,async-await,deadlock,C#,Async Await,Deadlock,在同一任务中使用多个等待时应小心。 我在尝试使用BlockingCollection.GetConsumingEnumerable()方法时遇到过这种情况。 最后是这个简化的测试 class TestTwoAwaiters { public void Test() { var t = Task.Delay(1000).ContinueWith(_ => Utils.WriteLine("task complete")); var w1 = F

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

}
我认为这里的问题是任务的继续将在一个线程上执行订阅服务器。 如果一个等待者执行阻塞操作(例如从
BlockingCollection.getconsumineGenumerable()
),它将阻塞其他等待者,他们无法继续工作。 我认为一个可能的解决方案是在等待任务之前调用
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),并且在await完成后,它已经在线程池线程上运行,它将不会更改线程以继续。这正是代码中发生的情况:在firstwaiter和secondwaiter中完成“await t t”语句后,continuation在这两种情况下都在同一个线程上运行,因为它是运行Delay(1000)的线程池线程。当然,当FirstWaiter执行它的延续时,SecondWaiter将阻塞,因为它的延续被发布到同一个线程池线程

编辑:如果您将使用ContinueWith而不是Wait,您可以“修复”您的问题(但仍请注意问题的注释):


这里有两种扩展方法,一种用于
任务
,另一种用于
任务
,确保
等待
后的异步继续。结果和异常按预期传播

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();
    }
}


更新:同步执行continuations的问题行为只影响.NET Framework。NET核心不受影响(延续在线程池线程中异步执行),因此上述解决方法仅适用于在.NET Framework上运行的应用程序。

您的代码代表什么问题?它运行时没有问题。问题是,当一个等待程序执行阻塞操作时,另一个无法执行任何操作您可能阻塞了UI线程,这与单个任务上的多个等待无关,对于多个任务的单等待,您也会遇到同样的问题。您不应该在异步方法中执行长时间运行的同步操作。有两个方法声称是异步的,但它们是同步运行的。别那么做,是的。但是其中一个等待者可以是一个专用循环,虽然等待的任务已完成,但此循环将阻止另一个等待者。在本例中,扩展的回答带有ContinueWith的行为。它很接近。但在您的示例中,每个wait都同步添加了一个continuation。所有continuation都在同一个线程上执行,这可能只是一个线程重用管理。在我的示例中,在任务完成之前添加了两个continuation。但我同意你的说法,两个等待者都是在一次延迟(1000)的延续中运行的。我有一段时间没有意识到这一点。我认为这是模糊的。是的,我认为这确实不清楚,需要记住。另一方面,它并没有在任何地方声明继续不能在同一线程上继续。好问题,我不知道为什么它被否决了。我认为ContinueWith()拆分continuence并安排它的操作。所以延迟(1000)延续有两个订户。当这个延续执行时,首先访问一个等待者,找到ContinueWIth(),它只是计划未来的操作。然后继续Delea(1000)访问其他等待者而不阻塞。
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();