Do I need to dispose of Tasks?

I get this question a lot:

“Task implements IDisposable and exposes a Dispose method.  Does that mean I should dispose of all of my tasks?”

Summary

Here’s my short answer to this question:
“No.  Don’t bother disposing of your tasks.”

Here’s my medium-length answer:
“No.  Don’t bother disposing of your tasks, not unless performance or scalability testing reveals that you need to dispose of them based on your usage patterns in order to meet your performance goals.  If you do find a need to dispose of them, only do so when it’s easy to do so, namely when you already have a point in your code where you’re 100% sure that they’re completed and that no one else is using them.”

And for those of you looking for a coffee-break read, here’s my long answer…

Why Task.Dispose?

At a high-level, the .NET Framework Design guidelines state that a type should implement IDisposable if it in turn holds onto other IDisposable resources.  And Task does.  Internally, Task may allocate a WaitHandle which can be used to wait on the Task to complete.  WaitHandle is IDisposable because it holds onto a SafeWaitHandle internally, which is IDisposable.  SafeWaitHandle wraps a native handle resource: if the SafeWaitHandle isn’t disposed, eventually its finalizer will get around to cleaning up the wrapped handle, but in the meantime its resources won’t be cleaned up and pressure will be put on the system.  By implementing IDisposable on Task, then, we enable developers concerned about aggressively cleaning up these resources to do so in a timely manner.

The problems

If every single Task allocated a WaitHandle, then it’d be a good idea for performance reasons to aggressively Dispose of Tasks.  But that’s not the case.  In reality, very few tasks actually have their WaitHandle allocated. In .NET 4, the WaitHandle was lazily-initialized in a few situations: if the Task’s ((IAsyncResult)task).AsyncWaitHandle explicitly-implemented interface property was accessed, or if the Task was used as part of a Task.WaitAll or Task.WaitAny call and the Task was not yet completed.  This made the answer to the “should I dispose” question slightly difficult, as if you had a lot of tasks being used with WaitAll/WaitAny, it might actually have been good to dispose of those tasks.

Also in .NET 4, once a Task was disposed, most of its members threw ObjectDisposedExceptions if they were accessed.  This made it difficult to cache completed tasks (which might be done for performance reasons), because if one consumer disposed of the task, another consumer would be unable to access important members of the task, like ContinueWith or its Result.

There’s another complication here, and that is that Tasks are fundamentally asynchronous primitives.  If the tasks are being used for parallelism, such as in a fork/join pattern, then it’s often easy to know when you’re done with them and when no one else is using them, e.g.

var tasks = new Task[3];
tasks[0] = Compute1Async();
tasks[1] = Compute2Async();
tasks[2] = Compute3Async();
Task.WaitAll(tasks);
foreach(var task in tasks) task.Dispose();

However, when using tasks for sequencing asynchronous operations, it’s often more difficult.  For example:

Compute1Async().ContinueWith(t1 =>
{
    t1.Dispose();
    …
});

This example successfully disposes of the Task returned from Compute1Async, but it neglects to Dispose of the Task returned from ContinueWith.  We could of course dispose of that one, too:

Compute1Async().ContinueWith(t1 =>
{
    t1.Dispose();
    …
}).ContinueWith(t2 => t2.Dispose());

but then we’re not disposing of the Task returned from the second ContinueWith.  You get the idea.  Even with the new async/await keywords in C# and Visual Basic, it’s still kludgy.  Consider a basic sequencing of operations, e.g.

string s1 = await Compute1Async();
string s2 = await Compute2Async(s1);
string s3 = await Compute3Async(s2);

If I wanted to Dispose of those Tasks, I’d need to rewrite this as something like the following:

string s1 = null, s2 = null, s3 = null;
using(var t1 = Compute1Async())
    s1 = await t1;
using(var t2 = Compute2Async(s1))
    s2 = await t2;
using(var t3 = Compute3Async(s2))
    s3 = await t3;

Awkward.

The solutions

Given the the lack of a need to dispose of most tasks and the awkwardness of doing so, for .NET 4.5 we’ve made several changes regarding Task.Dispose:

  1. We’ve made it much less likely that the Task’s WaitHandle will be allocated at all.  We’ve re-implemented WaitAll and WaitAny so that they don’t rely on a Task’s WaitHandle, and we’ve avoided using it internally for any of the new Task or async/await-related functionality introduced in .NET 4.5.  Thus, the only way the WaitHandle will be allocated is if you explicitly ask for the Task’s IAsyncResult.AsyncWaitHandle, and that should be quite rare.  This means that except in such very infrequent circumstances, disposing of a Task is completely unnecessary.
  2. We’ve made Tasks usable even after they’ve been disposed.  You can now use all of the public members of Task even after its disposal, and they’ll behave just as they did before disposal.  The only member you can’t use is IAsyncResult.AsyncWaitHandle, since that’s what actually gets disposed when you dispose of a Task instance; that property will continue to throw an ObjectDisposedException if you try to use it after the Task has been disposed.  This means you should feel freely comfortable caching completed Tasks, knowing that they’re observationally pure.  Additionally, moving forward, IAsyncResult usage should drop significantly now that we have async/await and the Task-based Async Pattern, and even for continued usage of IAsyncResult, usage of its AsyncWaitHandle is quite rare.
  3. For the new “.NET for Metro style apps” reference assemblies and surface area, Task doesn’t even implement IDisposable.  So for Metro style apps, or for portable libraries that span these apps, you don’t even have the option of disposing of Tasks, and that’s a good thing.

The guidance

So, this brings us back to the short answer: “No. Don’t bother disposing of your tasks.”  It’s often difficult to find a good place to do so, there’s almost never a reason to do so, and depending on your reference assemblies, you might not even be able to do so.