Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 302
Matt Burland Avatar asked May 02 '16 20:05

Matt Burland


People also ask

What is a continuation task?

A continuation task (also known just as a continuation) is an asynchronous task that's invoked by another task, known as the antecedent, when the antecedent finishes.

What is a delayed task?

Delay tasks can be used to stop the workflow for a specified duration. Delay tasks are always automated. The delay can be set to affect the planned or actual time of a task in a workflow. You can also set the date and time on which the next task in the workflow should be run.

Does task delay use a thread?

Task. Delay() is asynchronous. It doesn't block the current thread. You can still do other operations within current thread.

What is ContinueWith c#?

The ContinueWith function is a method available on the task that allows executing code after the task has finished execution. In simple words it allows continuation. Things to note here is that ContinueWith also returns one Task. That means you can attach ContinueWith one task returned by this method.


1 Answers

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.

like image 87
Lucas Trzesniewski Avatar answered Oct 02 '22 16:10

Lucas Trzesniewski