Matt Burland
Matt Burland

Reputation: 45155

Chaining tasks with delays

I have a need to keep track of a task and potentially queue up another task after some delay so the way I'm thinking of doing it looks something like this:

private Task lastTask;

public void DoSomeTask()
{
    if (lastTask == null) 
    {
        lastTask = Task.FromResult(false);
    }
    lastTask = lastTask.ContinueWith(t => 
    {
        // do some task
    }).ContinueWith(t => Task.Delay(250).Wait());
} 

My question is, if I do something like this, creating potentially long chains of tasks is will the older tasks be disposed or will they end up sticking around forever because the ContinueWith takes the last task as a parameter (so it's a closure). If so, how can I chain tasks while avoiding this problem?

Is there a better way to do this?

Upvotes: 8

Views: 758

Answers (3)

Yacoub Massad
Yacoub Massad

Reputation: 27871

Take a look at the source code of the ContinuationTaskFromTask class. It has the following code:

internal override void InnerInvoke()
{
    // Get and null out the antecedent.  This is crucial to avoid a memory
    // leak with long chains of continuations.
    var antecedent = m_antecedent;
    Contract.Assert(antecedent != null, 
        "No antecedent was set for the ContinuationTaskFromTask.");
    m_antecedent = null;

m_antecedent is the field that holds a reference to the antecedent ask. The developers here explicitly set it to null (after it is no longer needed) to make sure that there is no memory leak with long chains of continuations, which I guess is your concern.

Upvotes: 2

Lucas Trzesniewski
Lucas Trzesniewski

Reputation: 51430

Task.Delay(250).Wait()

You know you're doing something wrong when you use Wait in code you're trying to make asynchronous. That's one wasted thread doing nothing.

The following would be much better:

lastTask = lastTask.ContinueWith(t =>
{
    // do some task
}).ContinueWith(t => Task.Delay(250)).Unwrap();

ContinueWith returns a Task<Task>, and the Unwrap call turns that into a Task which will complete when the inner task does.

Now, to answer your question, let's take a look at what the compiler generates:

public void DoSomeTask()
{
    if (this.lastTask == null)
        this.lastTask = (Task) Task.FromResult<bool>(false);
    // ISSUE: method pointer
    // ISSUE: method pointer
    this.lastTask = this.lastTask
    .ContinueWith(
        Program.<>c.<>9__2_0
        ?? (Program.<>c.<>9__2_0 = new Action<Task>((object) Program.<>c.<>9, __methodptr(<DoSomeTask>b__2_0))))
    .ContinueWith<Task>(
        Program.<>c.<>9__2_1
        ?? (Program.<>c.<>9__2_1 = new Func<Task, Task>((object) Program.<>c.<>9, __methodptr(<DoSomeTask>b__2_1))))
    .Unwrap();
}

[CompilerGenerated]
[Serializable]
private sealed class <>c
{
    public static readonly Program.<>c <>9;
    public static Action<Task> <>9__2_0;
    public static Func<Task, Task> <>9__2_1;

    static <>c()
    {
        Program.<>c.<>9 = new Program.<>c();
    }

    public <>c()
    {
        base.\u002Ector();
    }

    internal void <DoSomeTask>b__2_0(Task t)
    {
    }

    internal Task <DoSomeTask>b__2_1(Task t)
    {
        return Task.Delay(250);
    }
}

This was decompiled with dotPeek in "show me all the guts" mode.

Look at this part:

.ContinueWith<Task>(
    Program.<>c.<>9__2_1
    ?? (Program.<>c.<>9__2_1 = new Func<Task, Task>((object) Program.<>c.<>9, __methodptr(<DoSomeTask>b__2_1))))

The ContinueWith function is given a singleton delegate. So, there's no closing over any variable there.

Now, there's this function:

internal Task <DoSomeTask>b__2_1(Task t)
{
    return Task.Delay(250);
}

The t here is a reference to the previous task. Notice something? It's never used. The JIT will mark this local as being unreachable, and the GC will be able to clean it. With optimizations enabled, the JIT will aggressively mark locals that are eligible for collection, even to the point that an instance method can be executing while the instance is being collected by the GC, if said instance method doesn't reference this in the code left to execute.

Now, one last thing, there's the m_parent field in the Task class, which is not good for your scenario. But as long as you're not using TaskCreationOptions.AttachedToParent you should be fine. You could always add the DenyChildAttach flag for extra safety and self-documentation.

Here's the function which deals with that:

internal static Task InternalCurrentIfAttached(TaskCreationOptions creationOptions)
{
    return (creationOptions & TaskCreationOptions.AttachedToParent) != 0 ? InternalCurrent : null;
}

So, you should be safe here. If you want to be sure, run a memory profiler on a long chain, and see for yourself.

Upvotes: 2

Servy
Servy

Reputation: 203825

if I do something like this, creating potentially long chains of tasks is will the older tasks be disposed

Tasks do not require explicit disposal, as they don't contain unmanaged resources.

will they end up sticking around forever because the ContinueWith takes the last task as a parameter (so it's a closure)

It's not a closure. A closure is an anonymous method using a variable from outside the scope of that anonymous method in its body. You're not doing that, so you're not closing over it. Each Task does however have a field where it keeps track of its parent, so the managed Task object will still be accessible if you're using this pattern.

Upvotes: 2

Related Questions