Home > Back-end >  Task invokes immediately upon being added to a List<Task> if the function has parenthesis. How
Task invokes immediately upon being added to a List<Task> if the function has parenthesis. How

Time:06-08

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(){...}

CodePudding user response:

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.

CodePudding user response:

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!

  • Related