【问题标题】:Multiple await on same task may cause blocking同一任务上的多个等待可能导致阻塞
【发布时间】:2015-09-23 19:52:04
【问题描述】:

在同一个任务上使用多个等待应该小心。 我在尝试使用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()。 它将继续分为两部分,阻塞操作将在新线程上执行。

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

【问题讨论】:

  • 你的代码代表什么问题?它运行没有问题。
  • 问题是当一个等待者执行阻塞操作时,另一个不能做任何事情
  • 你很可能阻塞了 UI 线程,它与单个任务上的多个等待无关,你会遇到与多个任务上的单个等待相同的问题。
  • 您不应该在异步方法中执行长时间运行的同步操作。您有两种方法声称是异步的,但它们是同步运行的。不要那样做。
  • 是的。但是其中一个等待者可以是一个专用循环,尽管等待的任务已经完成,但这个循环会阻塞其他等待者。

标签: c# async-await deadlock


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

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

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

【讨论】:

  • 在这种情况下用 ContinueWith 的行为扩展了答案。
  • 很接近了。但是在您的示例中,每个 await 都会同步添加一个延续。而所有延续都在同一个线程上执行的事实可能只是线程重用管理。在我的示例中,在任务完成之前添加了两个延续。但我同意你的观点,两个等待者都在一个 Delay(1000) 延续中运行。我有一段时间没有意识到这一点。我认为它是模糊的。
  • 是的,我认为这确实不清楚,请记住。另一方面,它没有在任何地方声称继续不能在同一个线程上继续。无论如何,这是个好问题,不知道为什么它被否决了。
  • 我认为 ContinueWith() 拆分延续,只是安排它的行动。所以 Delay(1000) 延续有两个订阅者。当这个延续执行时,它首先访问一个等待者,然后发现 ContinueWIth() 只是安排未来的行动。然后继续 Delea(1000) 访问另一个等待者而不阻塞。
【解决方案2】:

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-04-15
    • 1970-01-01
    • 2023-03-22
    • 1970-01-01
    • 1970-01-01
    • 2017-11-11
    • 1970-01-01
    • 2021-02-13
    相关资源
    最近更新 更多