Reputation: 1671
I want to start a collection of Task
objects at the same time and wait until all are complete. The following code shows my desired behaviour.
public class Program
{
class TaskTest
{
private Task createPauseTask(int ms)
{
// works well
return Task.Run(async () =>
// subsitution: return new Task(async () =>
{
Console.WriteLine($"Start {ms} ms pause");
await Task.Delay(ms);
Console.WriteLine($"{ms} ms are elapsed");
});
}
public async Task Start()
{
var taskList= new List<Task>(new[]
{
createPauseTask(1000),
createPauseTask(2000)
});
// taskList.ForEach(x => x.Start());
await Task.WhenAll(taskList);
Console.WriteLine("------------");
}
}
public static void Main()
{
var t = new TaskTest();
Task.Run(() => t.Start());
Console.ReadKey();
}
}
The output is:
Start 1000 ms pause
Start 2000 ms pause
1000 ms are elapsed
2000 ms are elapsed
------------
Now I wonder if it is possible to prepare my tasks and start them separately. In order to do that I change the first line in createPauseTask(int ms)
method from return Task.Run(async () =>
to return new Task(async () =>
and in the Start()
method I included a taskList.ForEach(x => x.Start());
before the WhenAll
. But then terrible things happen. The WhenAll
call completes immediately and the output is:
Start 2000 ms pause
Start 1000 ms pause
------------
1000 ms are elapsed
2000 ms are elapsed
Can someone show me what my mistake is and how to fix it?
Upvotes: 3
Views: 6423
Reputation: 8325
The issue is that the Task
constructor you're using accepts an Action
delegate. When you say
var task = new Task(async () => { /* Do things */ });
you are not creating a task that "does things"; you are instead created a task that executes an action that returns a task, and that (inner) task is what "does things". Creating this (inner) task is very quick, as the delegate returns the Task
at the first await
, and completes almost immediately. As the delegate is an Action
, the resultant Task
is effectively discarded, and can now no-longer be used to be awaited.
When you call await Task.WhenAll(tasks)
on your outer tasks, you are only waiting for the inner tasks to be created (almost immediate). The inner tasks continue running afterwards.
There are constructor overrides which allow you to do what you want, but your syntax will be a little more cumbersome, along the lines of Paulo's answer:
public static Task<Task> CreatePauseTask(int ms)
{
return new Task<Task>(async () =>
{
Console.WriteLine($"Start {ms} ms pause");
await Task.Delay(ms);
Console.WriteLine($"{ms} ms are elapsed");
});
}
You now have a task that does the same, but this time returns the inner Task
. To await the inner tasks, you can do this:
await Task.WhenAll(await Task.WhenAll(taskList));
The inner await returns the list of inner tasks, and the outer await await's them.
As also mentioned, though - creating unstarted tasks using constructors is an area you really should only be in if you have a highly-specific requirement where the more-standard practice of using Task.Run()
or simply calling task-returning methods does not meet the requirement (which it will do 99.99% of the time).
Most of those Task
constructors were created before async-await even existed, and are probably only still there for legacy reasons.
Edit: what might also help is to see the signatures of the 2 delegates, which the C# lambda notation allows us to - usually conveniently - overlook.
For new Task(async () => { /* Do things */ })
we have
async void Action() { }
(the void
being the main issue here).
For new Task<Task>(async () => { /* Do things */ })
we have
async Task Function() { }
Both lambdas are syntactically identical, yet semantically different.
Upvotes: 8
Reputation: 14836
I don't know why you're so found of Task.Run
. Every use of it in your code is just not needed.
I also wonder where you read that using the Task
constructor is a recommended practice.
Anyway, without using the Task
constructor, would be something like this:
class TaskTest
{
private async Task CreatePauseTask(int ms)
{
Console.WriteLine($"Start {ms} ms pause");
await Task.Delay(ms);
Console.WriteLine($"{ms} ms are elapsed");
}
public async Task Start()
{
var taskList = new List<Task>(new[]
{
CreatePauseTask(1000),
CreatePauseTask(2000)
});
await Task.WhenAll(taskList);
Console.WriteLine("------------");
}
}
static void Main()
{
var t = new TaskTest();
t.Start();
Console.ReadKey();
}
And that outputs:
Start 1000 ms pause
Start 2000 ms pause
1000 ms are elapsed
2000 ms are elapsed
------------
And using the Task
constructor would be:
class TaskTest
{
private Task CreatePauseTask(int ms)
{
return new Task<Task>(async () =>
{
Console.WriteLine($"Start {ms} ms pause");
await Task.Delay(ms);
Console.WriteLine($"{ms} ms are elapsed");
}, TaskCreationOptions.DenyChildAttach);
}
public async Task Start()
{
var taskList = new List<Task>(new[]
{
CreatePauseTask(1000),
CreatePauseTask(2000)
});
taskList.ForEach(t => t.Start(TaskScheduler.Default));
await Task.WhenAll(taskList);
Console.WriteLine("------------");
}
}
static void Main()
{
var t = new TaskTest();
t.Start();
Console.ReadKey();
}
And that outputs:
Start 1000 ms pause
Start 2000 ms pause
------------
1000 ms are elapsed
2000 ms are elapsed
The problem here is that Task.Run
understands Task
returning functions and the Task
constructor doesn't. So, using the Task
constructor will invoke the function which will return at the first blocking await (await Task.Delay(ms);
).
This is the expected behavior.
Upvotes: 2