Search code examples
c#async-awaittasktask-parallel-librarytaskcompletionsource

Can I guarantee runnable task continuations have been run?


Here's a significantly reduced test case from a piece of code I'm working on:

var i = 0;

var taskCompletionSource = new TaskCompletionSource<object>();
var task = taskCompletionSource.Task;

Task.Run(async () => 
{
    await task;
    i = 1;
});

// Synchronously complete `task`
taskCompletionSource.SetResult(null);

// ???

Console.WriteLine(i);

Assuming that this code runs in the context of an async method, what should replace // ??? to ensure that this code prints 1 rather than 0?

I believe I understand why as written the program will always print 0 -- the code is executing synchronously and nothing has yielded to the scheduler. But I am surprised to learn that

await Task.Yield();

doesn't suffice. Somewhat ironically (but perfectly understandably, given that it involves no asynchronous execution) neither does

await task;

On the other hand,

await Task.Delay(1);

does seem to be enough, but I'm not clear on whether that's a guarantee or an accident of timing.

To ask the question one more way: is there any (reasonable) code I can write which will guarantee that all continuations for task have run before proceeding?


Solution

  • Can I guarantee runnable task continuations have been run?

    By awaiting them.

    var i = 0;
    
    var taskCompletionSource = new TaskCompletionSource<object>();
    var task = taskCompletionSource.Task;
    
    var continuation = Task.Run(async () => 
    {
        await task;
        i = 1;
    });
    
    // Synchronously complete `task`
    taskCompletionSource.SetResult(null);
    
    // wait for the continuation
    await continuation;
    
    // ouputs 1
    Console.WriteLine(i);
    

    That works if you're withing an asynchronous method. If you're not, make it asynchronous. You can technically block, too, (.Wait() instead of await) but that invites deadlocks, so be careful.