1

I'm working with Tasks in C#. I'm having an issue with function invocation when I add my functions to the List. What happens is, instead of waiting until the Task.WhenAll(...) to invoke all functions at once, it invokes them immediately when added...only whenever I need to add them as a NameOfFunction() (so with parenthesis, with or without params).

This does not work and causes invocation immediately:

List<Task> rtcTasks = new List<Task>();

rtcTasks.Add(RunInitialProcess());
rtcTasks.Add(RunSecondaryProcess());

Task.WhenAll(rtcTasks).Wait();

This does work and invokes all when the process reaches Task.WhenAll(...);

List<Task> rtcTasks = new List<Task>();

rtcTasks.Add(Task.Run(RunInitialProcess));
rtcTasks.Add(Task.Run(RunSecondaryProcess));

Task.WhenAll(rtcTasks).Wait();

My issues is, I'd like to pass in functions that contain arguments that I can use for handling very easily without having to declare accessible objects in the current class I'm in.

Both functions are:

private async Task FunctionNameHere(){...}
  • 1
    Are these methods IO bound or CPU bound? If IO bound then it should be OK for the first one to start before the second as it will yield when it gets to the IO. If however they are CPU bound then you don't want them to return Task, and instead do something like `Task.Run(() => RunMethod(args))`. Either way you really should `await` the `WhenAll` – juharr Jun 07 '22 at 19:43
  • 1
    Why is it a problem to invoke the two asynchronous methods immediately? What is the benefit of postponing their invocation? – Theodor Zoulias Jun 07 '22 at 20:08
  • @TheodorZoulias I want them to run in parallel. If they are invoked immediately in C# and not via the task based method user juharr stated, then they will not run asynchronously – employeeWithCode Jun 07 '22 at 20:17
  • Are these methods comply with the [guidelines](https://learn.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap#initiating-an-asynchronous-operation "Task-based asynchronous pattern - Initiating an asynchronous operation")? Do they return quickly an incomplete `Task`, or they do all the work synchronously and return a completed `Task`? – Theodor Zoulias Jun 07 '22 at 20:23

2 Answers2

1

This does work and invokes all when the process reaches Task.WhenAll(...);.

Nope, it doesn't. The two asynchronous methods are invoked on ThreadPool threads a few microseconds after you add the tasks in the list, not when the Task.WhenAll method is invoked. Here is a minimal demonstration of this behavior:

Print("Before Task.Run");
Task task = Task.Run(() => DoWorkAsync());
Print("After Task.Run");
Thread.Sleep(1000);
Print("Before Task.WhenAll");
Task whenAllTask = Task.WhenAll(task);
Thread.Sleep(1000);
Print("Before await whenAllTask");
await whenAllTask;
Print("After await whenAllTask");

static async Task DoWorkAsync()
{
    Print("--Starting work");
    await Task.Delay(3000);
    Print("--Finishing work");
}

static void Print(object value)
{
    Console.WriteLine($@"{DateTime.Now:HH:mm:ss.fff} [{Thread.CurrentThread
        .ManagedThreadId}] > {value}");
}

Output:

20:25:50.080 [1] > Before Task.Run
20:25:50.101 [1] > After Task.Run
20:25:50.102 [4] > --Starting work
20:25:51.101 [1] > Before Task.WhenAll
20:25:52.101 [1] > Before await whenAllTask
20:25:53.103 [4] > --Finishing work
20:25:53.103 [4] > After await whenAllTask

Live demo.

The work is started 1 millisecond after creating the task, although The Task.WhenAll is invoked one whole second later.

The documentation of the Task.Run method says:

Queues the specified work to run on the thread pool and returns a proxy for the task returned by function.

Under normal conditions the ThreadPool is very responsive. It takes practically no time at all to execute the queued action.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
1

My issues is, I'd like to pass in functions that contain arguments that I can use for handling very easily without having to declare accessible objects in the current class I'm in.

Tasks only represent the return value of a method, they have no knowledge of the functions inputs.

If you're goal is to compile a generic list of Functions without knowledge of their arguments until later, then you're issue is you don't want a List<Task> you want a List<Func<...>>

Even then for this to work well without abusing things like reflection, all your Funcs need to have the same signature.

So lets say you have:

async Task FunctionOne(string myString, int myInt) { ... }
async Task FunctionTwo(string myString, int myInt) { ... }

But you don't have the string and int args read just yet to pass in, then you can store them as:

var myDelegates = new List<Func<string, int, Task>> { FunctionOne, FunctionTwo };

And then when you actually have all your args ready you could use Linq to fire off the tasks.

List<Task> = myTasks = myDelegates.Select(d => d(myStringArg, myIntArg)).ToList();
await Task.WhenAll(MyTasks);

Keep in mind Task.WhenAll(...) does not fire off the Tasks. The moment you have a Task object in memory it has already started.

It just happens to be in your code that Task.WhenAll is the next line of code, but the moment you do:

var myTask = SomeAsyncFunc(..args...)

The task has started.

So up above, the moment we call specifically .ToList() (which hydrates the IEnumerable into a List and invokes it greedily) the tasks all start up.

All await Task.WhenAll does is say "Block execution until all these tasks are complete"

The tasks may even already be complete before you have even invoked Task.WhenAll if they are fast ones!