0

I have long running processing that I want to perform in a background task. At the end of the task, I want to signal that it has completed. So essentially I have two async tasks that I want to run in the background, one after the other.

I am doing this with continuations, but the continuation is starting prior to the initial task completing. The expected behavior is that the continuation run only after the initial task has completed.

Here is some sample code that demonstrates the problem:

// Setup task and continuation
var task = new Task(async () =>
{
    DebugLog("TASK Starting");
    await Task.Delay(1000);     // actual work here
    DebugLog("TASK Finishing");
});

task.ContinueWith(async (x) =>
{
    DebugLog("CONTINUATION Starting");
    await Task.Delay(100);      // actual work here
    DebugLog("CONTINUATION Ending");
});

task.Start();

The DebugLog function:

static void DebugLog(string s, params object[] args)
{
    string tmp = string.Format(s, args);
    System.Diagnostics.Debug.WriteLine("{0}: {1}", DateTime.Now.ToString("HH:mm:ss.ffff"), tmp);
}

Expected Output:

TASK Starting
TASK Finishing
CONTINUATION Starting
CONTINUATION Ending

Actual Output:

TASK Starting
CONTINUATION Starting
CONTINUATION Ending
TASK Finishing

Again, my question is why is the continuation starting prior to the completion of the initial task? How do I get the continuation to run only after the completion of the first task?


Workaround #1

I can make the above code work as expected if I make the intial task synchronous - that is if I Wait on the Task.Delay like so:    

var task = new Task(() =>
{
    DebugLog("TASK Starting");
    Task.Delay(1000).Wait();     // Wait instead of await
    DebugLog("TASK Finishing");
});

For many reasons, it is bad to use Wait like this. One reason is that it blocks the thread, and this is something I want to avoid.


Workaround #2

If I take the task creation and move it into it's own function, that seems to work as well:

// START task and setup continuation
var task = Test1();
task.ContinueWith(async (x) =>
{
    DebugLog("CONTINUATION Starting");
    await Task.Delay(100);      // actual work here
    DebugLog("CONTINUATION Ending");
});

static public async Task Test1()
{
    DebugLog("TASK Starting");
    await Task.Delay(1000);     // actual work here
    DebugLog("TASK Finishing");
}

Credit for the above approach goes to this somewhat related (but not duplicate) question: Use an async callback with Task.ContinueWith


Workaround #2 is better than Workaround #1 and is likely the approach I'll take if there is no explanation for why my initial code above does not work.

3 답변


4

Your original code is not working because the async lambda is being translated into an async void method underneath, which has no built-in way to notify any other code that it has completed. So, what you're seeing is the difference between async void and async Task. This is one of the reasons that you should avoid async void.

That said, if it's possible to do with your code, use Task.Run instead of the Task constructor and Start; and use await rather than ContinueWith:

var task = Task.Run(async () =>
{
  DebugLog("TASK Starting");
  await Task.Delay(1000);     // actual work here
  DebugLog("TASK Finishing");
});
await task;
DebugLog("CONTINUATION Starting");
await Task.Delay(100);      // actual work here
DebugLog("CONTINUATION Ending");

Task.Run and await work more naturally with async code.


  • -1: The original code is not working because it does not use the Unwrap() method where necessary. The creation of an async void lambda is certainly not desired, but also nothing more than a side-effect of failing to use new Task<Task>(...) so the Unwrap() would work. - Sam Harwell
  • @280Z28: Yes, the original code could be hacked to use new Task<Task> and Unwrap; however, the new code would work because it would avoid an async void lambda. And it still wouldn't be as good as a Task.Run-based solution. - Stephen Cleary
  • +1 I ended up using Task.Run and the original continuation. Thank you for the insight into why my existing code doesn't work: that the lambda becomes async void. Of course everything made a lot more sense when you said that. So why am I using the continuation? Well, it is too much to try to explain here although it has a lot to do with this question. - chue x

4

You shouldn't be using the Task constructor directly to start tasks, especially when starting async tasks. If you want to offload work to be executed in the background use Task.Run instead:

var task = Task.Run(async () =>
{
    DebugLog("TASK Starting");
    await Task.Delay(1000);     // actual work here
    DebugLog("TASK Finishing");
});

About the continuation, it would be better to just append it's logic to the end of the lambda expression. But if you're adamant on using ContinueWith you need to use Unwrap to get the actual async task and store the it so you could handle exceptions:

task = task.ContinueWith(async (x) =>
{
    DebugLog("CONTINUATION Starting");
    await Task.Delay(100);      // actual work here
    DebugLog("CONTINUATION Ending");
}).Unwrap();

try
{
    await task;
}
catch
{
    // handle exceptions
}


  • +1 Thank you, Task.Run is what I needed. I never considered it because the MSDN continuation examples that I saw like this one use new Task. I am accepting Stephen's answer because he explains why my code above doesn't work: my initial task lambda becomes an async void instead of async Task. So why am I using the continuation? Well, it is too much to try to explain here although it has a lot to do with this question. - chue x

0

Change your code to

        // Setup task and continuation
        var t1 = new Task(() =>
        {
            Console.WriteLine("TASK Starting");
            Task.Delay(1000).Wait();     // actual work here
            Console.WriteLine("TASK Finishing");
        });

        var t2 = t1.ContinueWith((x) =>
        {
            Console.WriteLine("C1");
            Task.Delay(100).Wait();      // actual work here
            Console.WriteLine("C2");
        });

        t1.Start();

        // Exception will be swallow without the line below
        await Task.WhenAll(t1, t2);


  • I usually try to avoid the use of Wait because it blocks the executing thread, and it could potentially cause the thread to deadlock. See this post for more information about the deadlocks. The post uses Result instead of Wait, but both block the executing thread and both potentially cause deadlocks. - chue x

Linked


Related

Latest