简体   繁体   English

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

[英]Multiple await on same task may cause blocking

It should be careful to use several awaits on same Task.在同一个任务上使用多个等待应该小心。 I have encountered with such situation while trying to use BlockingCollection.GetConsumingEnumerable() method.我在尝试使用BlockingCollection.GetConsumingEnumerable()方法时遇到过这种情况。 And ends up with this simplified test.并以这个简化的测试结束。

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
    }

}

I think the problem here is the continuation of a task will execute subscribers on a one thread consequentially.我认为这里的问题是任务的延续将相应地在一个线程上执行订阅者。 And if one awaiter execute a blocking operation (such a yielding from BlockingCollection.GetConsumingEnumerable() ) it will block other awaiters and they couldn't continue their work.如果一个 awaiter 执行阻塞操作(例如从BlockingCollection.GetConsumingEnumerable() ),它将阻塞其他等待器并且他们无法继续他们的工作。 I think a possible solution will be to call ContinueWith() before await a task.我认为一个可能的解决方案是在等待任务之前调用ContinueWith() It will break a continuation to two parts and blocking operation will be executed on a new thread.它会将延续中断为两部分,并且将在新线程上执行阻塞操作。

Can someone confirm or disprove a possibility to await on a task several times.有人可以多次确认或反驳等待任务的可能性。 And if it is common then what is a proper way to get around blocking?如果它很常见,那么绕过阻塞的正确方法是什么?

Consider the following code:考虑以下代码:

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

If you run it, you will see the following output:如果运行它,您将看到以下输出:

1: 9, thread pool: False
2: 6, thread pool: True
3: 6, thread pool: True
4: 6, thread pool: True

You see here that if there is no SynchonizationContext (or you don't use ConfigureAwait) and after await completes it's already running on thread pool thread, it will not change thread for continuation.你在这里看到,如果没有 SynchonizationContext(或者你不使用 ConfigureAwait)并且等待完成后它已经在线程池线程上运行,它不会改变线程以继续。 This is exactly what happens in your code: after "await t" statement completes in FirstAwaiter and SecondAwaiter, continuation runs on the same thread in both cases, because it's thread pool thread where Delay(1000) ran.这正是您的代码中发生的情况:在 FirstAwaiter 和 SecondAwaiter 中的“await t”语句完成后,在这两种情况下,continuation 都在同一线程上运行,因为它是 Delay(1000) 运行的线程池线程。 And of course while FirstAwaiter performs it's continuation, SecondAwaiter will block since it's continuation is posted to the same thread pool thread.当然,当 FirstAwaiter 执行它的延续时,SecondAwaiter 将阻塞,因为它的延续被发布到同一个线程池线程。

EDIT: if you will use ContinueWith instead of await, you can kind of "fix" your problem (but note the comments to your question still):编辑:如果您将使用 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();
        });
    }
}

Here are two extension methods, one for Task and one for Task<TResult> , that ensure the asynchronous continuation after await .这里有两种扩展方法,一种用于Task ,一种用于Task<TResult> ,确保await之后的异步继续。 Results and exceptions are propagated as expected.结果和异常按预期传播。

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

Usage example:用法示例:

await t.ContinueAsync();

Update: The problematic behavior of executing continuations synchronously affects only the .NET Framework.更新:同步执行延续的问题行为仅影响 .NET Framework。 The .NET Core is not affected (the continuations are executed asynchronously in thread-pool threads), so the above workaround is useful only for applications running on .NET Framework. .NET Core 不受影响(延续在线程池线程中异步执行),因此上述解决方法仅适用于在 .NET Framework 上运行的应用程序。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM