Keeping Async Methods Alive

Keeping Async Methods Alive

Rate This
  • Comments 4

Consider a type that will print out a message when it’s finalized, and that has a Dispose method which will suppress finalization:

class DisplayOnFinalize : IDisposable
{
    public void Dispose() { GC.SuppressFinalize(this); }
    ~DisplayOnFinalize() { Console.WriteLine(“Finalized”); }
}

Now consider a simple usage of this class:

void Foo()
{
   
var tcs = new TaskCompletionSource<bool>();
    using(new DisplayOnFinalize())
    {
        tcs.Task.Wait();
    }
}

This method instantiates an instance of the finalizable class, and then blocks waiting for a task to be completed prior to disposing of the DisplayOnFinalize instance.  The task on which this code waits will never complete, and thus the calling thread will remain blocked at this location.  That thread’s stack will maintain a reference to the DisplayOnFinalize class, since if the wait were to complete the thread would need to invoke that instance’s Dispose method.  And as such, the message will never be printed out. 

Now, consider a small variation:

async void Foo()
{
    var tcs = new TaskCompletionSource<bool>();
    using(new DisplayOnFinalize())
    {

        await tcs.Task;
    }
}

The only differences here are that I’ve added the async keyword to the signature of my method, and I’m now asynchronously waiting for the task to complete (via the await keyword) rather than synchronously waiting for it to complete (via the Wait method).  However, this has a significant effect on behavior.  Of course, there’s the important difference that you’d expect, that we’re asynchronously waiting for the task and thus we’re not blocking the calling thread while waiting (forever) for the task to complete.  However, there’s a more subtle but nevertheless significant difference here… if you were to call this method repeatedly, you’d start to see “Finalized” getting printed out as the DisplayOnFinalize instances got garbage collected and finalized.

The async/await keywords tell the C#/Visual Basic compiler to rewrite your async method into a state machine, where the code that comes after the await is logically part of a continuation hooked up to the task (or, in general, the awaitable) being awaited.  The following isn’t exactly how the transformation happens, but you can think of the previous example as logically translating into something like the following (for the sake of this post, I’m leaving out lots of otherwise important details):

void Foo()
{
   
var tcs = new TaskCompletionSource<bool>();
    var d = new DisplayOnFinalize();
    tcs.Task.ContinueWith(delegate
    {
        d.Dispose();
    });

}

The code that comes after the await is in effect hooked up as a continuation, and in this case that code is the code to dispose of the DisplayOnFinalize instance.  Now, when the call to Foo returns, there’s no more reference via the thread’s stack to ‘d’.  The only reference to ‘d’ is in the closure/delegate hooked up to the task as a continuation.  If that task were rooted, that would be enough to keep the DisplayOnFinalize instance alive, but the task isn’t rooted.  The task is referred to by the TaskCompletionSource<bool> instance ‘tcs’ on the thread’s stack, but when the call to Foo goes away, so too does that reference to ‘tcs’.  And thus, all of these instances become available for garbage collection.

All of this serves to highlight an important fact: when you await something, it’s that something which needs to keep the async method’s execution alive via a reference to the continuation object that’s provided by ‘await’ to the awaited awaitable’s awaiter (try saying that ten times fast).  When you await Task.Run(…), the task you’re awaiting is rooted in the ThreadPool’s queues (or if the task is already executing, it’s rooted by the stack processing the task).  When you await Task.Delay(…), the task you’re awaiting is rooted by the underlying timer’s internal data structures.  When you await a task for an async I/O operation, the task is typically rooted by some data structure held by the I/O completion port that will be signaled when the async operation completes.  And so on. 

This all makes logical sense: in order to complete the task when the async operation completes, the thing that will be completing it must have a reference to it.  But it’s still something good to keep in mind… if you ever find that your async methods aren’t running to completion, consider whether awaitables you’re awaiting might be getting garbage collected before they complete.  In my previous post, I referred to a real bug that was discovered to be caused by a task never completing.  The way we happened across that bug was because a finalizable class similar in nature to the one shown previously was actually in use, and its finalizer was firing.  This led us to realize that it was invoked because the awaited task was getting garbage collected before it was completed, which was because of a a queue of work referencing completion sources, and that queue that was being cleared without canceling those associated tasks.

Leave a Comment
  • Please add 1 and 4 and type the answer here:
  • Post
  • I dont understood this " If that task were rooted, that would be enough to keep the DisplayOnFinalize instance alive, but the task isn’t rooted. ".

    Some thread(thread are roots, right?) should call my task, or it will never run right?

  • Hi Stephen. Great post!

    But can you please clarify a bit what you mean by saying "when you await something, it’s that something which needs to keep the async method’s execution alive via a reference to the continuation object [...]" ?

    To me it seems that that problem stems from the Task (or tcs in the first place) not being rooted. The cases you described (like ThreadPool, timer, I/O) are more of an exception to me - it just happens that the implementation keeps references to the tasks in some structures that are roots themselves.

    What's a general guideline for a user who creates tasks that way (besides that the user somewhere (?) should keep references to tasks that get hooked up to) ?

  • Felipe, yes, that's the point I was trying to make.  Something somewhere is doing something that will eventually complete and will pass those results along to the task.  In order to do that, it needs to have a reference to the task, or else it couldn't pass the results to it.

    Ay, thanks for the question, but I disagree about those cases being an exception.  Think of it this way.  The asynchronous operation is doing something, and when it's done, it needs to communicate those results back.  How is it going to do that?  It needs to have a reference to the TaskCompletionSource to share the results with.  If it didn't, it couldn't share the results with the consumer.  So, that asynchronous operation is responsible for holding onto that task and so that it can communicate back the results when it's done.  If that operation didn't somehow root the task, it would have no way to get to it and to communicate back the results.  If the async method were to somehow root itself but the task were never completed, we'd have a memory leak, because the async method and all of its state would never go away until the task completed, and the task is never completing.  Therefore, the async method's state will go away if the awaited task ever goes away without being completed.  If the async operation does its job correctly and maintains a reference to the task so that it can complete the task, then everything works correctly.  If the async operation doesn't do it job correctly and fails to root the task, then the async method becomes collectible rather than having an infinite memory leak.  In the case highlighted in this post, the fact that it became collectible is exactly why we were able to diagnose the problem of the task never completing... if we'd instead rooted the task while awaiting it, DisplayOnFinalize would have never been finalized, and we would instead have just seen a slow memory leak over time.

  • Stephen, thanks for the detailed explanation! Now I got what the message was :)

Page 1 of 1 (4 items)