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

时间:2015-09-23 19:52:04

标签: c# async-await deadlock

在同一个任务中使用几个等待应该小心。 我在尝试使用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.GetConsumingEnumerable()的屈服),它将阻止其他等待者,他们无法继续他们的工作。 我认为一个可能的解决方案是在等待任务之前调用ContinueWith()。 它将断开两个部分的延续,并且将在新线程上执行阻塞操作。

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

2 个答案:

答案 0 :(得分:1)

请考虑以下代码:

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完成它已经已经在线程池线程上运行之后,它将更改线程以继续。这正是您的代码中发生的事情:在"等待t"语句在FirstAwaiter和SecondAwaiter中完成,在两种情况下,继续在同一线程上运行,因为它的线程池线程在其中运行Delay(1000)。当然,当FirstAwaiter执行它的延续时,SecondAwaiter将阻止,因为它的延续被发布到同一个线程池线程。

编辑:如果您将使用ContinueWith而不是等待,您可以使用#34;修复"你的问题(但请注意你的问题的评论):

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

答案 1 :(得分:0)

这里有两种扩展方法,一种用于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上运行的应用程序有用。