简体   繁体   中英

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. 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. I think a possible solution will be to call ContinueWith() before await a task. 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. 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. And of course while FirstAwaiter performs it's continuation, SecondAwaiter will block since it's continuation is posted to the same thread pool thread.

EDIT: if you will use ContinueWith instead of await, you can kind of "fix" your problem (but note the comments to your question still):

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 . 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. 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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