5

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?

Dmitry Zuev
  • 79
  • 1
  • 6
  • 1
    What problem your code represents? It runs without issues. – Evk Sep 23 '15 at 20:01
  • The problem is while one awaiter executes blocking operation other can't do anything – Dmitry Zuev Sep 23 '15 at 20:08
  • 1
    You are likely blocking the UI thread, it has nothing to do with multiple awaits on a single task, you would have the same problem with single awaits on multiple tasks. – Scott Chamberlain Sep 23 '15 at 20:26
  • 3
    You just shouldn't be doing long running synchronous operations in asynchronous methods. You have two methods that claim to be asynchronous, but that run synchronously. Don't do that. – Servy Sep 23 '15 at 20:48
  • Yes. But one of the awaiters can be a dedicated loop and this loop will block other awaiter although the waited task is complete. – Dmitry Zuev Sep 23 '15 at 20:57
  • @DmitryZuev Yes, and Scott has explained why that's happening in your case specifically, but it's just a poor practice all around. If you were following good design patterns for asynchronous programming it wouldn't even come up. – Servy Sep 23 '15 at 21:06
  • @Servy Running synchronously in a method that claims to be asynchronous is not the problem. The problem is **blocking** within the method that claims to be asynchronous. – binki Jan 03 '18 at 19:03
  • @binki A method that blocks until the operation is finished is a synchronous operation. That's literally the definition of the word. So a method that runs synchronously *is* a method that blocks within the method. There is no difference between those statements. – Servy Jan 03 '18 at 19:09

2 Answers2

4

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.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
3

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();
        });
    }
}
Evk
  • 98,527
  • 8
  • 141
  • 191
  • Expanded answer a bit with behavior of ContinueWith in this case. – Evk Sep 23 '15 at 21:21
  • It is close. But in you example each of await adds a continuation synchronously. And the fact that all of continuation executes on the same thread probably just a thread reusing management. In my example two continuations are added before task is complete. But I agree with you that both awaiters are ran in one Delay(1000) continuation. And I couldn't realize that for some time. I think it is obscure. – Dmitry Zuev Sep 23 '15 at 21:29
  • Yes I think that is not clear indeed, something to keep in mind. On the other hand its not claimed anywhere that continuation cannot continue on the same thread. Good question anyway, not sure why it was downvoted. – Evk Sep 23 '15 at 21:35
  • I think ContinueWith() split continuation and just schedule it's action. So Delay(1000) continuation has two subscribers. When this continuation executes it first visit one of awaiters and there found ContinueWIth() which just schedule future action. Then continuation of Delea(1000) visit enother awaiter without blocking. – Dmitry Zuev Sep 23 '15 at 21:38