Say I have the following class:
class SomeClass
{
private TaskCompletionSource<string> _someTask;
public Task<string> WaitForThing()
{
_someTask = new TaskCompletionSource<string>();
return _someTask.Task;
}
//Other code which calls _someTask.SetResult(..);
}
Then elsewhere, I call
//Some code..
await someClassInstance.WaitForThing();
//Some more code
The //Some more code
won't be called until _someTask.SetResult(..)
is called. The calling-context is waiting around in memory somewhere.
However, let's say SetResult(..)
is never called, and someClassInstance
stops being referenced and is garbage collected. Does this create a memory leak? Or does .Net auto-magically know the calling-context needs to be disposed?
Updated, a good point by @SriramSakthivel, it turns out I've already answered a very similar question:
Why does GC collects my object when I have a reference to it?
So I'm marking this one as a community wiki.
If by the calling-context you mean the compiler-generated state machine object (which represents the state of the
async
method), then yes, it will indeed be finalized.Example:
Output:
IMO, this behavior is logical, but it still might be a bit unexpected that
something
gets finalized despite the fact that theusing
scope for it has never ended (and hence itsSomeDisposable.Dispose
has never been called) and that theTask
returned byTestSomethingAsync
is still alive and referenced inMain
.This could lead to some obscure bugs when coding system-level asynchronous stuff. It's really important to use
GCHandle.Alloc(callback)
on any OS interop callbacks which are not referenced outsideasync
methods. DoingGC.KeepAlive(callback)
alone at the end of theasync
method is not effective. I wrote about this in details here:Async/await, custom awaiter and garbage collector
On a side note, there's another kind of C# state machine: a method with
return yield
. Interestingly, along withIEnumerable
orIEnumerator
, it also implementsIDisposable
. Invoking itsDispose
will unwind anyusing
andfinally
statements (even in the case of incomplete enumerable sequence):Unlike this, with
async
methods there's no direct way of controlling the unwiding ofusing
andfinally
.You should ensure your tasks are always completed.
In the usual case, the "Other code which calls SetResult" is registered as a callback somewhere. E.g., if it's using unmanaged overlapped I/O, then that callback method is a GC root. Then that callback explicitly keeps
_someTask
alive, which keeps itsTask
alive, which keeps the delegate for//Some more code
alive.If the "Other code which calls SetResult" is not (directly or indirectly) registered as a callback, then I don't think there will be a leak. Note that this is not a supported use case, so this is not guaranteed. But I did create a memory profiling test using the code in your question and it does not appear to leak.