I've noticed firing up several Task.Delay() calls basically "at the same time" causes systematic and periodic long pauses in the execution. Not just in one thread, but all running threads.
Here's an old SO question, which describes probably the same issue: await Task.Delay(foo); takes seconds instead of ms
I hope it's ok to re-surface this with a fresh take, since the problem still exists and I haven't found any other workaround than "use Thread.Sleep", which doesn't really work in all cases.
Here's a test code:
static Stopwatch totalTime = new Stopwatch();
static void Main(string[] args)
{
Task[] tasks = new Task[100];
totalTime.Start();
for (int i = 0; i < 100; i++)
{
tasks[i] = TestDelay(1000, 10, i);
}
Task.WaitAll(tasks);
}
private static async Task TestDelay(int loops, int delay, int id)
{
int exact = 0;
int close = 0;
int off = 0;
Stopwatch stopwatch = new Stopwatch();
for (int i = 0; i < loops; i++)
{
stopwatch.Restart();
await Task.Delay(delay);
long duration = stopwatch.ElapsedMilliseconds;
if (duration == delay) ++exact;
else if (duration < delay + 10) ++close;
else
{
//This is seen in chunks for all the tasks at once!
Console.WriteLine(totalTime.ElapsedMilliseconds + " ------ " + id + ": " + duration + "ms");
++off;
}
}
Console.WriteLine(totalTime.ElapsedMilliseconds + " -DONE- " + id + " Exact: " + exact + ", Close: " + close + ", Off:" + off);
}
By running the code, there will be 1-3 points in time, when all of the N tasks will block/hang/something for significantly more than 10ms, more like 100-500ms. This happens to all tasks, and at the same time. I've added relevant logging, in case someone wants to try it and fiddle with the numbers.
Finally the obvious question is: Why is this happening, and is there any way to avoid it? Can anyone run the code and NOT get the delays?
Tested with dotnetcore 3.1 and net 5.0. Ran on MacOS and Linux.
Changing min threads doesn't have any effect on this.
Just for laughs, I tried SemaphoreSlim.WaitAsync(millis) (on an always unsignaled semaphore), which funnily enough has the same problem.
EDIT: Here's a sample output:
136 ------ 65: 117ms
136 ------ 73: 117ms
160 ------ 99: 140ms
... all 100 of these
161 ------ 3: 144ms
Similar output is printed later in the execution as well.
These lines are printed when a task delay takes over 10ms more than requested.
So the first number is the point in time, which is almost the same for all tasks, so I assume it's due to the same hang in execution. The second number is just the task id to tell them apart. Last number is the stopwatch-given delay, which is significantly more than the 10ms.
It can be 10-20ms easily, but 10x is not due to inaccuracy.
I've tried to look into GC problems, but it doesn't happen during a manual GC.Collect(), and when it does happen I don't see changes in heapdump. It's still a possibility, but I'm lost at pinpointing it.
I'll do the unthinkable, and answer my own question, just in case anyone else stumbles upon this.
First, thanks to #paulomorgado for pointing me towards thread pool latency. That indeed is the problem, if you fire up hundreds of Task.Delay() calls in a short period of time.
The way I solved this, was to create a separate Thread, which keeps track of requested delays and uses TaskCompletionSource to enable asynchronous awaits on the delays.
E.g. create a struct with three fields: start time, delay duration and a TaskCompletionSource. Have the thread loop through these (in a lock) and whenever a duration has expired, mark the task done with TaskCompletionSource.SetResult().
Now you can have a custom async Delay(millis) method that
creates a new struct
adds it to a "requested delays" list (lock)
awaits for the task completion
remove the struct from the list (lock)
return
A custom TaskScheduler with the needed threads might be a fancier solution, but I found this approach simple and clean. And it seems to do the trick, especially since you can have more than one thread going through all the delays for extra efficiency. Obviously happy to have this approach murdered with any flaws you might notice.
Please note that this approach probably only makes sense if your code is filled with asynchronous delays for some reason (like mine).
EDIT Quick sample code for the relevant parts. This needs some optimizing in regards of how locks, loops, news, and lists are handled, but even with this, I can see a HUGE improvement.
With ridiculously short delays (say 10ms), this shows error at 80ms max (tried with 5 threads), where with Task.Delay it's at least 100ms, up to 500ms. With longer, reasonable, delays (100ms+) this is almost flawless, whereas Task.Delay() slaps with the same 100-500ms surprise delay at least in the beginning.
private struct CustomDelay
{
public TaskCompletionSource Completion;
public long Started;
public long Delay;
}
//Your favorite data structure here. Using List for clarity. Note: using separate blocks based on delay granularity might be a good idea.
private static List<CustomDelay> _requestedDelays = new List<CustomDelay>();
//Create threads from this. Sleep can be longer if there are several threads.
private static void CustomDelayHandler()
{
while (_running)
{
Thread.Sleep(10); //To avoid busy loop
lock (_delayListLock)
{
for (int i = 0; i < _requestedDelays.Count; ++i)
{
CustomDelay delay = _requestedDelays[i];
if (!delay.Completion.Task.IsCompleted)
{
if (TotalElapsed() - delay.Started >= delay.Delay)
{
delay.Completion.SetResult();
}
}
}
}
}
}
//Use this instead of Task.Delay(millis)
private static async Task Delay(int ms)
{
if (ms <= 0) return;
CustomDelay delay = new CustomDelay()
{
Completion = new TaskCompletionSource(),
Delay = ms,
Started = TotalElapsed()
};
lock (_delayListLock)
{
_requestedDelays.Add(delay);
}
await delay.Completion.Task;
lock (_delayListLock)
{
_requestedDelays.Remove(delay);
}
}
Here is my attempt to reproduce your observations. I am creating 100 tasks, and each task is awaiting repeatedly a 10 msec Task.Delay in a loop. The actual duration of each Delay is measured with a Stopwatch, and is used to update a dictionary that holds the occurrences of each duration (all measurements with the same integer duration are aggregated in a single entry in the dictionary). The total duration of the test is 10 seconds.
ThreadPool.SetMinThreads(100, 100);
const int nominalDelay = 10;
var cts = new CancellationTokenSource(10000); // Duration of the test
var durations = new ConcurrentDictionary<long, int>();
var tasks = Enumerable.Range(1, 100).Select(n => Task.Run(async () =>
{
var stopwatch = new Stopwatch();
while (true)
{
stopwatch.Restart();
try { await Task.Delay(nominalDelay, cts.Token); }
catch (OperationCanceledException) { break; }
long duration = stopwatch.ElapsedMilliseconds;
durations.AddOrUpdate(duration, _ => 1, (_, count) => count + 1);
}
})).ToArray();
Task.WaitAll(tasks);
var totalTasks = durations.Values.Sum();
var totalDuration = durations.Select(pair => pair.Key * pair.Value).Sum();
Console.WriteLine($"Nominal delay: {nominalDelay} msec");
Console.WriteLine($"Min duration: {durations.Keys.Min()} msec");
Console.WriteLine($"Avg duration: {(double)totalDuration / totalTasks:#,0.0} msec");
Console.WriteLine($"Max duration: {durations.Keys.Max()} msec");
Console.WriteLine($"Total tasks: {totalTasks:#,0}");
Console.WriteLine($"---Occurrences by Duration---");
foreach (var pair in durations.OrderBy(e => e.Key))
{
Console.WriteLine($"Duration {pair.Key,2} msec, Occurrences: {pair.Value:#,0}");
}
I run the program on .NET Core 3.1.3, in Release version without the debugger attached. Here are the results:
(Try it on fiddle)
Nominal delay: 10 msec
Min duration: 9 msec
Avg duration: 15.2 msec
Max duration: 40 msec
Total tasks: 63,418
---Occurrences by Duration---
Duration 9 msec, Occurrences: 165
Duration 10 msec, Occurrences: 11,373
Duration 11 msec, Occurrences: 21,299
Duration 12 msec, Occurrences: 2,745
Duration 13 msec, Occurrences: 878
Duration 14 msec, Occurrences: 375
Duration 15 msec, Occurrences: 252
Duration 16 msec, Occurrences: 7
Duration 17 msec, Occurrences: 16
Duration 18 msec, Occurrences: 102
Duration 19 msec, Occurrences: 110
Duration 20 msec, Occurrences: 1,995
Duration 21 msec, Occurrences: 14,839
Duration 22 msec, Occurrences: 7,347
Duration 23 msec, Occurrences: 1,269
Duration 24 msec, Occurrences: 166
Duration 25 msec, Occurrences: 136
Duration 26 msec, Occurrences: 264
Duration 27 msec, Occurrences: 47
Duration 28 msec, Occurrences: 1
Duration 36 msec, Occurrences: 5
Duration 37 msec, Occurrences: 8
Duration 38 msec, Occurrences: 9
Duration 39 msec, Occurrences: 7
Duration 40 msec, Occurrences: 3
Running the program on .NET Framework 4.8.3801.0 produces similar results.
TL;DR, I was not able to reproduce the 100-500 msec durations you observed.
I use multiple BackgroundWorker control to run some task as multithreading. But I found that when using more than 4 BackgroundWoker, the one from 4th forward delay more than second to actually execute from when calling RunWorkerAsync.
Could help me how can I start all backgroundworker immediately?
class TaskLog
{
public int task_id;
public DateTime call_time;
public DateTime start_time;
public DateTime end_time;
}
BackgroundWorker[] bws = new BackgroundWorker[18];
int[] tasks = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
Queue<TaskLog> queueTask;
TaskLog[] records;
int task_complete = 999;
private void button1_Click(object sender, EventArgs e)
{
if (task_complete < tasks.Length) return;
task_complete = 0;
records = tasks.Select(t => new TaskLog { task_id = t }).ToArray();
queueTask = new Queue<TaskLog>(records);
for (int i = 0; i < bws.Length && queueTask.Count > 0; ++i)
{
bws[i] = new BackgroundWorker();
bws[i].DoWork += new DoWorkEventHandler(download_vid_work);
bws[i].RunWorkerCompleted += new RunWorkerCompletedEventHandler(download_vid_complete);
var x = queueTask.Dequeue();
x.call_time = DateTime.Now;
bws[i].RunWorkerAsync(x);
//Debug.WriteLine("start " + x.task_id);
}
}
void download_vid_work(object sender, DoWorkEventArgs e)
{
var record = (TaskLog)e.Argument;
record.start_time = DateTime.Now;
//Debug.WriteLine("actually start " + record.task_id);
Thread.Sleep(10000); // 10s
e.Result = record;
}
void download_vid_complete(object sender, RunWorkerCompletedEventArgs e)
{
var record = (TaskLog)e.Result;
record.end_time = DateTime.Now;
//Debug.WriteLine("complete " + item.ToString());
++task_complete;
if (task_complete == tasks.Length)
{
Debug.WriteLine("all tasks are completed!");
foreach (var r in records)
{
Debug.WriteLine("task {0} delay time: {1}", r.task_id, (r.start_time - r.call_time).TotalMilliseconds.ToString("0,0"));
}
}
else if (queueTask.Count > 0)
{
var bw = (BackgroundWorker)sender;
var nextTask = queueTask.Dequeue();
bw.RunWorkerAsync(nextTask);
nextTask.call_time = DateTime.Now;
}
}
Here is log result after run:
all tasks are completed!
task 1 delay time: 22
task 2 delay time: 24
task 3 delay time: 24
task 4 delay time: 23
task 5 delay time: 1,005
task 6 delay time: 2,002
task 7 delay time: 3,003
task 8 delay time: 4,003
task 9 delay time: 5,004
task 10 delay time: 6,005
The ThreadPool class, which manages the thread pool threads used for BackgroundWorker (and other needs), does not maintain an infinite number of worker threads ready to run.
You can configure the actual number of idle threads (*), using the ThreadPool.SetMinThreads() method. As you can see in your case, when you initially start your program, there are four idle threads ready to accept work right away. The default number of idles threads depends on a variety of things related to the OS version and configuration.
Once there are more queued work items for the thread pool than there are threads to service them, the ThreadPool class does not create new threads right away. It waits for a short period of time (as you can see from your test, one second), on the assumption that it's possible one of the other tasks may finish soon and it will be able to reuse that thread rather than going to all the trouble of creating yet another thread (which incurs its own overhead and would even slow down the work of the threads already running).
In general, you should avoid overriding the default values for the thread pool, as they are generally set correctly given your OS version, hardware, etc. For example, it won't help to have more CPU-bound threads running than you have CPU cores on the machine. Letting the ThreadPool class decide when and how to run your worker threads is usually the best approach.
(*) The above is a bit of an over-simplification. In newer versions of .NET, the minimum number of threads may or may not actually exist at any given time. If work items are queued when there are fewer than the minimum number, ThreadPool will immediately create new threads as needed up to the minimum. Beyond that, it then shifts to its more elaborate creation and scheduling logic.
I'm using an API client that is completely asynchrounous, that is, each operation either returns Task or Task<T>, e.g:
static async Task DoSomething(int siteId, int postId, IBlogClient client)
{
await client.DeletePost(siteId, postId); // call API client
Console.WriteLine("Deleted post {0}.", siteId);
}
Using the C# 5 async/await operators, what is the correct/most efficient way to start multiple tasks and wait for them all to complete:
int[] ids = new[] { 1, 2, 3, 4, 5 };
Parallel.ForEach(ids, i => DoSomething(1, i, blogClient).Wait());
or:
int[] ids = new[] { 1, 2, 3, 4, 5 };
Task.WaitAll(ids.Select(i => DoSomething(1, i, blogClient)).ToArray());
Since the API client is using HttpClient internally, I would expect this to issue 5 HTTP requests immediately, writing to the console as each one completes.
int[] ids = new[] { 1, 2, 3, 4, 5 };
Parallel.ForEach(ids, i => DoSomething(1, i, blogClient).Wait());
Although you run the operations in parallel with the above code, this code blocks each thread that each operation runs on. For example, if the network call takes 2 seconds, each thread hangs for 2 seconds w/o doing anything but waiting.
int[] ids = new[] { 1, 2, 3, 4, 5 };
Task.WaitAll(ids.Select(i => DoSomething(1, i, blogClient)).ToArray());
On the other hand, the above code with WaitAll also blocks the threads and your threads won't be free to process any other work till the operation ends.
Recommended Approach
I would prefer WhenAll which will perform your operations asynchronously in Parallel.
public async Task DoWork() {
int[] ids = new[] { 1, 2, 3, 4, 5 };
await Task.WhenAll(ids.Select(i => DoSomething(1, i, blogClient)));
}
In fact, in the above case, you don't even need to await, you can just directly return from the method as you don't have any continuations:
public Task DoWork()
{
int[] ids = new[] { 1, 2, 3, 4, 5 };
return Task.WhenAll(ids.Select(i => DoSomething(1, i, blogClient)));
}
To back this up, here is a detailed blog post going through all the
alternatives and their advantages/disadvantages: How and Where Concurrent Asynchronous I/O with ASP.NET Web API
I was curious to see the results of the methods provided in the question as well as the accepted answer, so I put it to the test.
Here's the code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace AsyncTest
{
class Program
{
class Worker
{
public int Id;
public int SleepTimeout;
public async Task DoWork(DateTime testStart)
{
var workerStart = DateTime.Now;
Console.WriteLine("Worker {0} started on thread {1}, beginning {2} seconds after test start.",
Id, Thread.CurrentThread.ManagedThreadId, (workerStart-testStart).TotalSeconds.ToString("F2"));
await Task.Run(() => Thread.Sleep(SleepTimeout));
var workerEnd = DateTime.Now;
Console.WriteLine("Worker {0} stopped; the worker took {1} seconds, and it finished {2} seconds after the test start.",
Id, (workerEnd-workerStart).TotalSeconds.ToString("F2"), (workerEnd-testStart).TotalSeconds.ToString("F2"));
}
}
static void Main(string[] args)
{
var workers = new List<Worker>
{
new Worker { Id = 1, SleepTimeout = 1000 },
new Worker { Id = 2, SleepTimeout = 2000 },
new Worker { Id = 3, SleepTimeout = 3000 },
new Worker { Id = 4, SleepTimeout = 4000 },
new Worker { Id = 5, SleepTimeout = 5000 },
};
var startTime = DateTime.Now;
Console.WriteLine("Starting test: Parallel.ForEach...");
PerformTest_ParallelForEach(workers, startTime);
var endTime = DateTime.Now;
Console.WriteLine("Test finished after {0} seconds.\n",
(endTime - startTime).TotalSeconds.ToString("F2"));
startTime = DateTime.Now;
Console.WriteLine("Starting test: Task.WaitAll...");
PerformTest_TaskWaitAll(workers, startTime);
endTime = DateTime.Now;
Console.WriteLine("Test finished after {0} seconds.\n",
(endTime - startTime).TotalSeconds.ToString("F2"));
startTime = DateTime.Now;
Console.WriteLine("Starting test: Task.WhenAll...");
var task = PerformTest_TaskWhenAll(workers, startTime);
task.Wait();
endTime = DateTime.Now;
Console.WriteLine("Test finished after {0} seconds.\n",
(endTime - startTime).TotalSeconds.ToString("F2"));
Console.ReadKey();
}
static void PerformTest_ParallelForEach(List<Worker> workers, DateTime testStart)
{
Parallel.ForEach(workers, worker => worker.DoWork(testStart).Wait());
}
static void PerformTest_TaskWaitAll(List<Worker> workers, DateTime testStart)
{
Task.WaitAll(workers.Select(worker => worker.DoWork(testStart)).ToArray());
}
static Task PerformTest_TaskWhenAll(List<Worker> workers, DateTime testStart)
{
return Task.WhenAll(workers.Select(worker => worker.DoWork(testStart)));
}
}
}
And the resulting output:
Starting test: Parallel.ForEach...
Worker 1 started on thread 1, beginning 0.21 seconds after test start.
Worker 4 started on thread 5, beginning 0.21 seconds after test start.
Worker 2 started on thread 3, beginning 0.21 seconds after test start.
Worker 5 started on thread 6, beginning 0.21 seconds after test start.
Worker 3 started on thread 4, beginning 0.21 seconds after test start.
Worker 1 stopped; the worker took 1.90 seconds, and it finished 2.11 seconds after the test start.
Worker 2 stopped; the worker took 3.89 seconds, and it finished 4.10 seconds after the test start.
Worker 3 stopped; the worker took 5.89 seconds, and it finished 6.10 seconds after the test start.
Worker 4 stopped; the worker took 5.90 seconds, and it finished 6.11 seconds after the test start.
Worker 5 stopped; the worker took 8.89 seconds, and it finished 9.10 seconds after the test start.
Test finished after 9.10 seconds.
Starting test: Task.WaitAll...
Worker 1 started on thread 1, beginning 0.01 seconds after test start.
Worker 2 started on thread 1, beginning 0.01 seconds after test start.
Worker 3 started on thread 1, beginning 0.01 seconds after test start.
Worker 4 started on thread 1, beginning 0.01 seconds after test start.
Worker 5 started on thread 1, beginning 0.01 seconds after test start.
Worker 1 stopped; the worker took 1.00 seconds, and it finished 1.01 seconds after the test start.
Worker 2 stopped; the worker took 2.00 seconds, and it finished 2.01 seconds after the test start.
Worker 3 stopped; the worker took 3.00 seconds, and it finished 3.01 seconds after the test start.
Worker 4 stopped; the worker took 4.00 seconds, and it finished 4.01 seconds after the test start.
Worker 5 stopped; the worker took 5.00 seconds, and it finished 5.01 seconds after the test start.
Test finished after 5.01 seconds.
Starting test: Task.WhenAll...
Worker 1 started on thread 1, beginning 0.00 seconds after test start.
Worker 2 started on thread 1, beginning 0.00 seconds after test start.
Worker 3 started on thread 1, beginning 0.00 seconds after test start.
Worker 4 started on thread 1, beginning 0.00 seconds after test start.
Worker 5 started on thread 1, beginning 0.00 seconds after test start.
Worker 1 stopped; the worker took 1.00 seconds, and it finished 1.00 seconds after the test start.
Worker 2 stopped; the worker took 2.00 seconds, and it finished 2.00 seconds after the test start.
Worker 3 stopped; the worker took 3.00 seconds, and it finished 3.00 seconds after the test start.
Worker 4 stopped; the worker took 4.00 seconds, and it finished 4.00 seconds after the test start.
Worker 5 stopped; the worker took 5.00 seconds, and it finished 5.00 seconds after the test start.
Test finished after 5.00 seconds.
You can use the Task.WhenAll function, to which you can pass any number of tasks. The Task.WhenAll returns a new task that will complete when all the tasks have completed. Be sure to wait asynchronously on Task.WhenAll, to avoid blocking your UI thread:
public async Task DoSomethingAsync() {
Task[] tasks = new Task[numTasks];
for (int i = 0; i < numTasks; i++)
{
tasks[i] = DoChildTaskAsync();
}
await Task.WhenAll(tasks);
// Code here will execute on UI thread
}
Since the API you're calling is async, the Parallel.ForEach version doesn't make much sense. You shouldnt use .Wait in the WaitAll version since that would lose the parallelism Another alternative if the caller is async is using Task.WhenAll after doing Select and ToArray to generate the array of tasks. A second alternative is using Rx 2.0
Parallel.ForEach requires a list of user-defined workers and a non-async Action to perform with each worker.
Task.WaitAll and Task.WhenAll require a List<Task>, which are by definition asynchronous.
I found RiaanDP's response very useful to understand the difference, but it needs a correction for Parallel.ForEach. Not enough reputation to respond to his comment, thus my own response.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace AsyncTest
{
class Program
{
class Worker
{
public int Id;
public int SleepTimeout;
public void DoWork(DateTime testStart)
{
var workerStart = DateTime.Now;
Console.WriteLine("Worker {0} started on thread {1}, beginning {2} seconds after test start.",
Id, Thread.CurrentThread.ManagedThreadId, (workerStart - testStart).TotalSeconds.ToString("F2"));
Thread.Sleep(SleepTimeout);
var workerEnd = DateTime.Now;
Console.WriteLine("Worker {0} stopped; the worker took {1} seconds, and it finished {2} seconds after the test start.",
Id, (workerEnd - workerStart).TotalSeconds.ToString("F2"), (workerEnd - testStart).TotalSeconds.ToString("F2"));
}
public async Task DoWorkAsync(DateTime testStart)
{
var workerStart = DateTime.Now;
Console.WriteLine("Worker {0} started on thread {1}, beginning {2} seconds after test start.",
Id, Thread.CurrentThread.ManagedThreadId, (workerStart - testStart).TotalSeconds.ToString("F2"));
await Task.Run(() => Thread.Sleep(SleepTimeout));
var workerEnd = DateTime.Now;
Console.WriteLine("Worker {0} stopped; the worker took {1} seconds, and it finished {2} seconds after the test start.",
Id, (workerEnd - workerStart).TotalSeconds.ToString("F2"), (workerEnd - testStart).TotalSeconds.ToString("F2"));
}
}
static void Main(string[] args)
{
var workers = new List<Worker>
{
new Worker { Id = 1, SleepTimeout = 1000 },
new Worker { Id = 2, SleepTimeout = 2000 },
new Worker { Id = 3, SleepTimeout = 3000 },
new Worker { Id = 4, SleepTimeout = 4000 },
new Worker { Id = 5, SleepTimeout = 5000 },
};
var startTime = DateTime.Now;
Console.WriteLine("Starting test: Parallel.ForEach...");
PerformTest_ParallelForEach(workers, startTime);
var endTime = DateTime.Now;
Console.WriteLine("Test finished after {0} seconds.\n",
(endTime - startTime).TotalSeconds.ToString("F2"));
startTime = DateTime.Now;
Console.WriteLine("Starting test: Task.WaitAll...");
PerformTest_TaskWaitAll(workers, startTime);
endTime = DateTime.Now;
Console.WriteLine("Test finished after {0} seconds.\n",
(endTime - startTime).TotalSeconds.ToString("F2"));
startTime = DateTime.Now;
Console.WriteLine("Starting test: Task.WhenAll...");
var task = PerformTest_TaskWhenAll(workers, startTime);
task.Wait();
endTime = DateTime.Now;
Console.WriteLine("Test finished after {0} seconds.\n",
(endTime - startTime).TotalSeconds.ToString("F2"));
Console.ReadKey();
}
static void PerformTest_ParallelForEach(List<Worker> workers, DateTime testStart)
{
Parallel.ForEach(workers, worker => worker.DoWork(testStart));
}
static void PerformTest_TaskWaitAll(List<Worker> workers, DateTime testStart)
{
Task.WaitAll(workers.Select(worker => worker.DoWorkAsync(testStart)).ToArray());
}
static Task PerformTest_TaskWhenAll(List<Worker> workers, DateTime testStart)
{
return Task.WhenAll(workers.Select(worker => worker.DoWorkAsync(testStart)));
}
}
}
The resulting output is below. Execution times are comparable. I ran this test while my computer was doing the weekly anti virus scan. Changing the order of the tests did change the execution times on them.
Starting test: Parallel.ForEach...
Worker 1 started on thread 9, beginning 0.02 seconds after test start.
Worker 2 started on thread 10, beginning 0.02 seconds after test start.
Worker 3 started on thread 11, beginning 0.02 seconds after test start.
Worker 4 started on thread 13, beginning 0.03 seconds after test start.
Worker 5 started on thread 14, beginning 0.03 seconds after test start.
Worker 1 stopped; the worker took 1.00 seconds, and it finished 1.02 seconds after the test start.
Worker 2 stopped; the worker took 2.00 seconds, and it finished 2.02 seconds after the test start.
Worker 3 stopped; the worker took 3.00 seconds, and it finished 3.03 seconds after the test start.
Worker 4 stopped; the worker took 4.00 seconds, and it finished 4.03 seconds after the test start.
Worker 5 stopped; the worker took 5.00 seconds, and it finished 5.03 seconds after the test start.
Test finished after 5.03 seconds.
Starting test: Task.WaitAll...
Worker 1 started on thread 9, beginning 0.00 seconds after test start.
Worker 2 started on thread 9, beginning 0.00 seconds after test start.
Worker 3 started on thread 9, beginning 0.00 seconds after test start.
Worker 4 started on thread 9, beginning 0.00 seconds after test start.
Worker 5 started on thread 9, beginning 0.01 seconds after test start.
Worker 1 stopped; the worker took 1.00 seconds, and it finished 1.01 seconds after the test start.
Worker 2 stopped; the worker took 2.00 seconds, and it finished 2.01 seconds after the test start.
Worker 3 stopped; the worker took 3.00 seconds, and it finished 3.01 seconds after the test start.
Worker 4 stopped; the worker took 4.00 seconds, and it finished 4.01 seconds after the test start.
Worker 5 stopped; the worker took 5.00 seconds, and it finished 5.01 seconds after the test start.
Test finished after 5.01 seconds.
Starting test: Task.WhenAll...
Worker 1 started on thread 9, beginning 0.00 seconds after test start.
Worker 2 started on thread 9, beginning 0.00 seconds after test start.
Worker 3 started on thread 9, beginning 0.00 seconds after test start.
Worker 4 started on thread 9, beginning 0.00 seconds after test start.
Worker 5 started on thread 9, beginning 0.00 seconds after test start.
Worker 1 stopped; the worker took 1.00 seconds, and it finished 1.00 seconds after the test start.
Worker 2 stopped; the worker took 2.00 seconds, and it finished 2.00 seconds after the test start.
Worker 3 stopped; the worker took 3.00 seconds, and it finished 3.00 seconds after the test start.
Worker 4 stopped; the worker took 4.00 seconds, and it finished 4.00 seconds after the test start.
Worker 5 stopped; the worker took 5.00 seconds, and it finished 5.01 seconds after the test start.
Test finished after 5.01 seconds.
All the answers are for running the same function.
The following code works for calling different functions. Just put your regular Task.Run() inside an array and call with Task.WhenAll():
await Task.WhenAll(new Task[] {
Task.Run(() => Func1(args)),
Task.Run(() => Func2(args))
});
The question is 10 years old and OP was asking about C# 5.
As of today, there is one more option: Parallel.ForEachAsync method that was introduced in .NET 6.
Here is an example based on the OP's code:
int[] ids = new[] { 1, 2, 3, 4, 5 };
await Parallel.ForEachAsync(ids, async (i,token) => await DoSomething(1, i, blogClient));
This is completely asynchronous and doesn't block any threads.
Additionally, it might be better than Task.WaitAll and Task.WhenAll approaches because they don't limit the number of threads running in parallel. So if you have a huge array it can eat up all your RAM. Parallel.ForEachAsync allows you to specify parallelism degree like so:
var options = new ParallelOptions { MaxDegreeOfParallelism = 4 };
await Parallel.ForEachAsync(ids, options, async (i,token) => await DoSomething(1, i, blogClient));
This way you have only 4 threads running in parallel.
I just want to add to all great answers above,
that if you write a library it's a good practice to use ConfigureAwait(false)
and get better performance, as said here.
So this snippet seems to be better:
public static async Task DoWork()
{
int[] ids = new[] { 1, 2, 3, 4, 5 };
await Task.WhenAll(ids.Select(i => DoSomething(1, i))).ConfigureAwait(false);
}
A full fiddle link here.