none
Task Dispose best practice?

    Question

  • When working with Tasks, especially continuations and nested tasks (attached or unattached), what would be the recommended approach for disposing the tasks?

    e.g. in a situation something along these lines...

    Task.Factory.StartNew(() =>
    {
    	// do things
    }).ContinueWith(t =>
    {
    	// do things
    	Task.Factory.StartNew(() =>
    	{
    		// do things
    	});
    })
    .ContinueWith(t => { // do things });

    Thanks,
    Matt
    Monday, November 16, 2009 7:05 AM

Answers

  • Hi Matt-

    Task.Dispose exists due to Task potentially wrapping an event handle used when waiting on the task to complete, in the event the waiting thread actually has to block (as opposed to spinning or potentially executing the task it's waiting on).  If all you're doing is using continuations, that event handle will never be allocated and the value of Dispose is greatly diminished. Additionally, if you're only creating a few tasks here and there, even if you are waiting on them, the pressure on the finalizer to clean up after the tasks would be fairly minimal, due to there being only a few wait handles to be cleaned up.

    In short, as is typically the case in .NET, dispose aggressively *if* it's easy and correct to do based on the structure of your code.  If you start having to do strange gyrations in order to Dispose (or in the case of Tasks, use additional synchronization to ensure it's safe to dispose, since Dispose may only be used once a task has completed), it's likely better to rely on finalization to take care of things.  In the end, it's best to measure, measure, measure to see if you actually have a problem before you go out of your way to make the code less sightly in order to implement clean-up functionality.

    I hope that helps.
    Monday, November 16, 2009 8:40 AM
    Owner
  • Hi Kazi-

    For #1, you put "waits" in quotes because you're using a continuation, right?  You're never accessing ((IAsyncResult)task).AsyncWaitHandle? Given your description, the WaitHandle will almost certainly not be allocated.  In .NET 4, the only times it gets allocated is if a WaitAll/WaitAny falls back to a true blocking wait, or if you access the AsyncWaitHandle property (the underlying ManualResetEventSlim gets allocated in a few more places, like when using Wait, but it lazily allocates the WaitHandle, and otherwise just uses Monitor, which does not require disposal).  For a future release, we're actually also looking into whether we can reduce further how often this allocation happens, reducing Dispose to almost a nop, but we'll see if we get there.

    Saturday, February 05, 2011 4:52 PM
    Owner

All replies

  • Hi Matt-

    Task.Dispose exists due to Task potentially wrapping an event handle used when waiting on the task to complete, in the event the waiting thread actually has to block (as opposed to spinning or potentially executing the task it's waiting on).  If all you're doing is using continuations, that event handle will never be allocated and the value of Dispose is greatly diminished. Additionally, if you're only creating a few tasks here and there, even if you are waiting on them, the pressure on the finalizer to clean up after the tasks would be fairly minimal, due to there being only a few wait handles to be cleaned up.

    In short, as is typically the case in .NET, dispose aggressively *if* it's easy and correct to do based on the structure of your code.  If you start having to do strange gyrations in order to Dispose (or in the case of Tasks, use additional synchronization to ensure it's safe to dispose, since Dispose may only be used once a task has completed), it's likely better to rely on finalization to take care of things.  In the end, it's best to measure, measure, measure to see if you actually have a problem before you go out of your way to make the code less sightly in order to implement clean-up functionality.

    I hope that helps.
    Monday, November 16, 2009 8:40 AM
    Owner
  • Hi Stephen,

    Thanks for the prompt and helpful reply.

    If I'm understanding you correctly (and reading the code in reflector correctly), you're saying that if I wait on a task then the internal CompletedEvent property may be accessed which would allocate a ManualResetEventSlim object. The tasks themselves don't end up on the finalization queue, it's the SafeHandle owned by the WaitHandle owned by the ManualResetEventSlim.

    Our ASP.NET app currently makes around 5,000 remote calls per hour which may last between 5 to 30 seconds each (typically 10-15 seconds) and we're looking at scaling up to handle 10,000 on the same hardware. We've seen clustered requests cause more than 70 concurrent remote calls. Naturally we're keen to use the new PFX APIs to maximise the efficiency of our throughput and thread usage, especially in areas such as the remote calls, aggregation and post-processing. I don't think our usage will cause the finalizer much grief, although I will of course keep an eye on the metrics as you suggest.

    Btw, I found your Patterns of Parallel Programming whitepaper very useful and well written, although I would have liked to see more coverage of how tasks can help with I/O operations :)

    Thanks,
    Matt
    Tuesday, November 17, 2009 2:31 AM
  • Interesting. This is the first time I see somebody suggesting not to call Dispose in favor of clean code.
    While it might work well for the current version what happens if Task internals are changed in next version and Dispose becomes more important or even very important? The existing code might not perform well anymore or it migh not work at all. This is the hazard of not calling Dispose that I see.
    Just thinking out loud...
    Miha Markic [MVP C#] http://blog.rthand.com
    Tuesday, November 17, 2009 10:18 AM

  • I've experienced a great difference in the use of Task.Factory.StartNew(() =>.. 
    - When running on Server 2008, the GC worked fine and the automatic cleanup went well. 
    - On Server 2003 however, extensive use of Task.Factory.StartNew() caused a massive leak; the thread count were simply building up to massive amounts and never disposed...

    For our specific purpose we had to switch to the Threadpool to avoid this.

    Rune
    Thursday, August 26, 2010 10:24 AM
  • Stephen,

    Thanks for the advice. However, I must say that I wish Microsoft would create an IOptionalDisposable interface for use when Dispose is actually optional!

    Having a large number of exceptions to the rule of "call Dispose if it is implemented" will cause newer developers to simply not call Dispose.

    Can we at the very least get the documentation to clearly state that the Task class has a finalizer which will eventually clean up like Dispose would have done?


    John Saunders
    WCF is Web Services. They are not two separate things.
    Use WCF for All New Web Service Development, instead of legacy ASMX or obsolete WSE
    Use File->New Project to create Web Service Projects
    Friday, September 17, 2010 2:45 PM
  • Hi John-

    I'm not sure what IOptionalDispose would really mean.  The IDisposable pattern already makes Dispose optional: if you don't call it, the pattern dictates that finalization should be used to pick up the slack.  That's the case with Task, too.  If it's not too hard for you to call Dispose, you should do so.  But if trying to call Dispose forces you to bend over backwards and do uncomfortable acts in your code just to be able to Dispose, it's typically better to just let finalizers do their job.  Note, too, that in the case of Task, Task doesn't actually have a finalizer, as it itself doesn't directly hold unmanaged resources.  Rather, during its life it may allocate a WaitHandle, which has a finalizer, so if you don't dispose a Task and it had allocated a WaitHandle (due to your waiting on the Task), the WaitHandle's finalizer will clean up after the WaitHandle.

    Sunday, September 19, 2010 3:13 AM
    Owner
  • Hi Stephen,

    The way I understand IDisposable, what you say is wrong.  Calling Dispose() is not optional in general; it's a correctness issue.  The GC isn't required (and will not in practice) find finalizable objects if under no memory pressure.  That means you can deadlock on failing to release locks, for instance.  It's unfortunate that types exist which aren't truly disposable yet implement IDisposable, but what you're saying sounds worse: Task is sometimes disposable.

    When exactly does a Task need disposing? Why isn't this documented in big red letters? What are the consequences of failing to do so?


    --Eamon
    Thursday, January 06, 2011 9:10 AM
  • Disposing of a Task is not necessary for correctness.  All it does is in turn call Dispose on the underlying WaitHandle that may have been created if you ever waited on the task or if you ever explicitly accessed its WaitHandle, e.g. ((IAsyncResult)task).AsyncWaitHandle.  Dispose is effectively a nop if that WaitHandle was never allocated.  And if you don't call Dispose, that just means that if the WaitHandle was allocated, the WaitHandle's finalizer will eventually be invoked to clean up after it.

    Thursday, January 06, 2011 11:56 PM
    Owner
  • Hi Stephen,

    I have a question on how we know whether the waithandle is invoked or not? In my model I am trying to implement a retry-semantic on async operations. So I retry if it fails (until maxRetryCount reached), or return result if it passes. My implementation is substantially inspired by your Iterator example, where you have an outer task that invokes mutiple "inner" tasks. Since it is retrying I use a System.Threading.Timer to delay for X milliseconds. My questions is, will this mechanism trigger any WaitHandle either in teh inner tasks or the outer task, because of any of the following reasons:

    1. My task continuation "waits" on the yield return in the iterator
    2. I create the inner task inside teh TimerCallback and chain it with the outer task using a TaskCompletionSource

    Your blogs on TPL is really really helpful! Thanks!!

    Friday, February 04, 2011 1:53 AM
  • Hi Kazi-

    For #1, you put "waits" in quotes because you're using a continuation, right?  You're never accessing ((IAsyncResult)task).AsyncWaitHandle? Given your description, the WaitHandle will almost certainly not be allocated.  In .NET 4, the only times it gets allocated is if a WaitAll/WaitAny falls back to a true blocking wait, or if you access the AsyncWaitHandle property (the underlying ManualResetEventSlim gets allocated in a few more places, like when using Wait, but it lazily allocates the WaitHandle, and otherwise just uses Monitor, which does not require disposal).  For a future release, we're actually also looking into whether we can reduce further how often this allocation happens, reducing Dispose to almost a nop, but we'll see if we get there.

    Saturday, February 05, 2011 4:52 PM
    Owner
  • For a future release, we're actually also looking into whether we can reduce further how often this allocation happens, reducing Dispose to almost a nop, but we'll see if we get there.


    Did you get there? In the available releases of .NET 4.5, did you manage to reduce the number of cases where Task.Dispose() should be called?
    Tuesday, November 08, 2011 9:41 AM