I had cases when this did not process the items in parallel and had to use Task.Run
to force it to run on different threads.
Most likely these cases were associated with methods that have an asynchronous contract, but their implementation is synchronous. Like this method for example:
static async Task NotAsync(string item)
{
Thread.Sleep(10000); // Simulate a CPU-bound calculation, or a blocking I/O operation
await Task.CompletedTask;
}
Any thread that invokes this method will be blocked for 10 seconds, and then it will be handed back an already completed task. Although the contract of the NotAsync
method is asynchronous (it has an awaitable return type), its actual implementation is synchronous because it does all the work during the invocation. So when you try to create multiple tasks by invoking this method:
toProcess.ForEach(s => tasks.Add(NotAsync(s)));
...the current thread will be blocked for 10 seconds * number of tasks. When these tasks are created they are all completed, so waiting for their completion will cause zero waiting:
Task.WaitAll(tasks.ToArray()); // Waits for 0 seconds
By wrapping the NotAsync
in a Task.Run
you ensure that the current thread will not be blocked, because the NotAsync
will be invoked on the ThreadPool
.
toProcess.ForEach(s => tasks.Add(Task.Run(() => NotAsync(s))));
The Task.Run
returns immediately a Task
, with guaranteed zero blocking.
It should be noted that writing asynchronous methods with synchronous implementations violates Microsoft's guidelines:
An asynchronous method that is based on TAP can do a small amount of work synchronously, such as validating arguments and initiating the asynchronous operation, before it returns the resulting task. Synchronous work should be kept to the minimum so the asynchronous method can return quickly.
But sometimes even Microsoft violates this guideline. That's because violating this one is better than violating the guideline about not exposing asynchronous wrappers for synchronous methods. In order words exposing APIs that call Task.Run
internally in order to give the impression of being asynchronous, is an even greater sin than blocking the current thread.