Stevie
Stevie

Reputation: 196

Create multiple tasks, don't await them, but have them run sequentially

I'm trying to have 2 tasks run one after the other, but I don't want to wait for them to finish since I'll be doing that later. Initially I had an implementation with ContinueWith but the double await was bothering me and I don't see Unwrap() as a significant improvement.

I decided to do a PoC to create the 2 tasks separately and have another task dispatched to manage them while I return the 2 initial tasks but weirdly the tasks become marked as completed as soon as I hit the await Task.Delay() which basically means that in reality they're running concurrently. This is the code:

var task1 = new Task(async ()=>
{
    Console.WriteLine("Task1 Start");
    await Task.Delay(5000);
    Console.WriteLine("Task1 STOP");
});
var task2 = new Task(async () => 
{
    Console.WriteLine("Task2 Start");
    await Task.Delay(5000);
    Console.WriteLine("Task2 STOP");
});
var taskParent = Task.Run(async () => 
{
    Console.WriteLine("starting 1");
    task1.Start();
    await task1;
    Console.WriteLine("starting 2");
    task2.Start();
    await task2;
});

Console.WriteLine("BEGIN await parent");
await taskParent;
Console.WriteLine("END await parent");

and the output is

BEGIN await parent
starting 1
Task1 Start
starting 2
Task2 Start
END await parent
Task2 STOP
Task1 STOP

So I go from my desire to have task2 begin after task1 to it finishing before task1 does. I can't see a reason why calling await Task.Delay would mark the tasks as complete. Am I missing something?

EDIT To simplify my requirements since there seems to be a bit of confusion. The tasks must be returned before they are awaited, and the second task must run after the first. Some other thread will want the result later on and if it is completed fine if not it will await. I need to be able to await the second task and expect that the first one is executed before the second one.

Upvotes: 2

Views: 1567

Answers (4)

Theodor Zoulias
Theodor Zoulias

Reputation: 43996

You'll hear frequently that using the Task constructor is not recommended, and it's a wise advice. It's easier to create a Func<Task>, and invoke it when you want to start the task, and it's also safer. The Task constructor has the same hidden gotchas with the ContinueWith method: it doesn't understand async delegates, and it requires to specify explicitly the scheduler when you start it. But if you know positively that the Task constructor is the best tool for your problem, here is how you can use it:

Task<Task> taskTask1 = new Task<Task>(async () =>
{
    Console.WriteLine("Task1 Start");
    await Task.Delay(5000);
    Console.WriteLine("Task1 STOP");
});

Task<Task> taskTask2 = new Task<Task>(async () =>
{
    Console.WriteLine("Task2 Start");
    await Task.Delay(5000);
    Console.WriteLine("Task2 STOP");
});

Task taskParent = Task.Run(async () =>
{
    Console.WriteLine("starting 1");
    taskTask1.Start(TaskScheduler.Default);
    await taskTask1.Unwrap();
    Console.WriteLine("starting 2");
    taskTask2.Start(TaskScheduler.Default);
    await taskTask2.Unwrap();
});

Console.WriteLine("BEGIN await parent");
await taskParent;
Console.WriteLine("END await parent");

Output:

BEGIN await parent
starting 1
Task1 Start
Task1 STOP
starting 2
Task2 Start
Task2 STOP
END await parent

Notice that you don't need to Unwrap the Task.Run, because this method understands async delegates, and does the unwrapping automatically for you.

Notice also the TaskScheduler.Default passed as argument to the Start method. Not specifying the scheduler makes your code depended on the ambient TaskScheduler.Current, and might generate warnings in the presence of the CA2008 analyzer.

Upvotes: 2

Panagiotis Kanavos
Panagiotis Kanavos

Reputation: 131729

Tasks aren't threads. There's never a good reason to create a task through its constructor and try to "start" it later. A Task is a Promise that something will complete in the future and may not even be executable. For example, Task.Delay uses a timer to signal a TaskCompletionSource.

It's impossible to control execution through Start, again because tasks arent' threads. Start only schedules a task for execution, it doesn't actually run it. There's no guarantee the tasks will run in the order they were scheduled.

await doesn't execute a task either, it awaits an already active task to complete, without blocking the calling thread. You don't need to await a task to make it execute. You only need to await it when you want to get its results, or wait for it to finish.

As for the question itself, it's unclear what the problem is. If the question is how to execute some async functions in sequence without awaiting the entire sequence, the easiest way would be to put them in their own async method, store its task in a variable and await it when needed :

async Task GetAnImageAsync()
{
    //Load a URL from DB using Dapper
    var url=await connection.QueryFirstOrDefault<string>("select top 1 URLs from Pictures");
    //Get the image
    var image=await client.GetByteArrayAsync(url);
    //Save it
    await File.WriteAllBytesAsync("blah.jpg",image);
}

...

async Task DoSomethingElse()
{
    var imageTask=GetAnImageAsync();
    //Do some other work
    ...
    //Only await at the end
    await imageTask();
}

Upvotes: 3

Zdeněk Jel&#237;nek
Zdeněk Jel&#237;nek

Reputation: 2933

Given that all task constructors accept Action or variants of thereof, doing

var task1 = new Task(async ()=>
{
    Console.WriteLine("Task1 Start");
    await Task.Delay(5000);
    Console.WriteLine("Task1 STOP");
});

task1.Start();
await task1;

is not dissimilar to doing

void EntryPoint()
{
    CallAsync();
}

async void CallAsync()
{
    Console.WriteLine("Task1 Start");
    await Task.Delay(5000);
    Console.WriteLine("Task1 STOP");
}

The CallAsync from above will run until first suspension (await Task.Delay(5000)) upon which it registers a Timer callback and returns to the caller which promptly returns, completely unaware of any async semantics.

If the application is still alive by the time the Timer under Task.Delay fires, it will run the continuation based on current TaskScheduler and SynchronizationContext as usual, writing Task1 STOP to the console.

If you need to run tasks sequentially without awaiting them, either use ContinueWith or implement a custom TaskScheduler to do this for you.

Or even better, as suggested by @Panagiotis Kanavos, create a stand-alone async method which runs the sequence of tasks and await its result:

async Task PerformThingsAsync()
{
    var task = RunMyTasksAsync();

    // Do things

    await task;
}

async Task RunTasksAsync()
{
    await RunFistTaskAsync();
    await RunSecondTaskAsync();
    // ...
}

Upvotes: 0

MikolajR
MikolajR

Reputation: 101

Basically in c# a task created is always running, therefore if you create task, and not awaiting it, then it will be running in separate thread most likely. Take a look at: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/

To archive sequential execution you either have to do a kind of scheduler for them or implement sequence on some sort of locking mechanism.

Continue with is the easiest method to guarantee that.

Upvotes: 0

Related Questions