0

I have a simple taken form MS documentation implementation of generic throttle function.

 public async Task RunTasks(List<Task> actions, int maxThreads)
    {
        var queue = new ConcurrentQueue<Task>(actions);
        var tasks = new List<Task>();
        for (int n = 0; n < maxThreads; n++)
        {
            tasks.Add(Task.Run(async () =>
            {
                while (queue.TryDequeue(out Task action))
                {
                    action.Start();
                    await action;
                    int i = 9; //this should not be reached.
                }
            }));
        }
        await Task.WhenAll(tasks);
    }

To test it I have a unit test:

 [Fact]
    public async Task TaskRunningLogicThrottles1()
    {
        var tasks = new List<Task>();
        const int limit = 2;

        for (int i = 0; i < 2000; i++)
        {
            var task = new Task(async () => {
                await Task.Delay(-1);
            });
            tasks.Add(task);
        }
        await _logic.RunTasks(tasks, limit);
    }

Since there is a Delay(-1) in the tasks this test should never complete. The line "int y = 9;" in the RunTasks function should never be reached. However it does and my whole function fails to do what it is supposed to do - throttle execution. If instead or await Task.Delay() I used synchronous Thread.Sleep ot works as exptected.

user2555515
  • 779
  • 6
  • 20
  • Interesting that you have a `List` that is called `actions.` Are you sure it isn't supposed to be a `List` (or its async equivalent, `List>`)? – John Wu Mar 15 '21 at 19:35
  • I don't know were you got this code from. Sure doesn't seem like an example. Maybe this is usefull. https://www.codeproject.com/Tips/1264928/Throttling-Multiple-Tasks-to-Process-Requests-in-C – M1sterPl0w Mar 15 '21 at 19:36
  • What happens if you change `tasks.Add(task);` to `tasks.Add(Task.Delay(-1));` – Andy Mar 15 '21 at 19:49
  • @Andy no that will not work. With adding Task.Delay(-1) you will get an invalidOperationException. – M1sterPl0w Mar 15 '21 at 20:03
  • @M1sterPl0w -- why? It's just a list of tasks. – Andy Mar 15 '21 at 20:04
  • @andy Yes it worked ( had to remove the action.Start() part ) so now I got even more lost. I thought chaining of async/await is the way to go but cannot explain what's going on here. – user2555515 Mar 15 '21 at 20:09
  • @user2555515 you but that is not the point is it? When you don't start a task it will never reach Task.Delay. – M1sterPl0w Mar 15 '21 at 20:11
  • @M1sterPl0w Not exactly, because Task.Delay(-1) starts the task automatically so calling Start on it again fails. The puzzling question is why the parent task that is supposed to wait for its child task (Task.Delay() ) returns before the child finishes. – user2555515 Mar 15 '21 at 20:20

1 Answers1

2

The Task class has no constructor that accepts async delegates, so the async delegate you passed to it is async void. This is a common trap. Just because the compiler allows us to add the async keyword to any lambda, doesn't mean that we should. We should only pass async lambdas to methods that expect and understand them, meaning that the type of the parameter should be a Func with a Task return value. For example Func<Task>, or Func<Task<T>>, or Func<TSource, Task<TResult>> etc.

What you could do is to pass the async lambda to a Task<TResult> constructor, in which case the TResult would be resolved as Task. In other words you could create nested Task<Task> instances:

var taskTask = new Task<Task>(async () =>
{
    await Task.Delay(-1);
});

This way you would have a cold outer task, that when started would create the inner task. The work required to create a task is negligible, so the inner task will be created instantly. The inner task would be a promise-style task, like all tasks generated by async methods. A promise-style task is always hot on creation. It cannot be created in a cold state like a delegate-based task. Calling its Start method results to an InvalidOperationException.

Creating cold tasks and nested tasks is an advanced technique that is used rarely in practice. The common technique for starting promise-style tasks on demand is to pass them around as async delegates (Func<Task> instances in various flavors), and invoke each delegate at the right moment.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • @user2555515 The difference is in what you do with it not how you create it, which wasn't shown, and which isn't a good idea anyway (hence why I assume it wasn't shown). The duplicate shows implementations of the approach described in the last paragraph here. – Servy Mar 15 '21 at 20:27
  • Thanks but I fail to see any difference between your code and mine (with exception that you provided explicit task type, while in mine it was implicit. However they both behave the same (i.e. fail to throttle). As for shown/not shown: The entire code is in the original post so I do not quite understand what you mean by "(hence why I assume it wasn't shown)." – user2555515 Mar 16 '21 at 19:12
  • @user2555515 are you responding to me or to Servy? – Theodor Zoulias Mar 16 '21 at 21:40