Only a few threads executing at a time [duplicate] - c#

This question already has answers here:
ThreadPool not starting new Thread instantly
(2 answers)
Closed 1 year ago.
I am working on a program where we are constantly starting new threads to go off and do a piece of work. We noticed that even though we might have started 10 threads only 3 or 4 were executing at a time. To test it out I made a basic example like this:
private void startThreads()
{
for (int i = 0; i < 100; i++)
{
//Task.Run(() => someThread());
//Thread t = new Thread(() => someThread());
//t.Start();
ThreadPool.QueueUserWorkItem(someThread);
}
}
private void someThread()
{
Thread.Sleep(1000);
}
Simple stuff right? Well, the code creates the 100 threads and they start to execute... but only 3 or 4 at a time. When they complete the next threads start to execute. I would have expected that almost all of them start execution at the same time. For 100 threads (each with a 1 second sleep time) it takes about 30 seconds for all of them to complete. I would have thought it would have taken far less time than this.
I have tried using Thread.Start, ThreadPool and Tasks, all give me the exact same result. If I use ThreadPool and check for the available number of threads each time a thread runs there are always >2000 available worker threads and 1000 available async threads.
I just used the above as a test for our code to try and find out what is going on. In practice, the code spawns threads all over the place. The program is running at less than 5% CPU usage but is getting really slow because the threads aren't executing quick enough.

Yes you may only have a few threads running at the same time. That how a ThreadPool works. It doesn't necessarily run all the threads at the same time. It would queue them up fast, but then leave it to the ThreadPool to handle when each thread runs.
If you want to ensure all 100 threads run simultaneously you can use:
ThreadPool.SetMinThreads(100, 100);
For example, see the code below, this is the result without the thread pool min size:
No MinThreads
internal void startThreads()
{
ThreadPool.GetMaxThreads(out int maxThread, out int completionPortThreads);
stopwatch.Start();
var result = Parallel.For(0, 20, (i) =>
{
ThreadPool.QueueUserWorkItem(someThread, i);
});
while (!result.IsCompleted) { }
Console.WriteLine("Queueing completed...");
}
private void someThread(Object stateInfo)
{
int threadNum = (int)stateInfo;
Console.WriteLine(threadNum + " started.");
Thread.Sleep(10);
Console.WriteLine(threadNum + " finnished.");
}
Result (No MinThreads)
9 started.
7 started.
11 started.
10 started.
1 finnished.
12 started.
9 finnished.
13 started.
2 finnished.
4 finnished.
15 started.
3 finnished.
8 finnished.
16 started.
10 finnished.
6 finnished.
19 started.
0 finnished.
14 started.
5 finnished.
7 finnished.
17 started.
18 started.
11 finnished.
With MinThreads
internal void startThreads()
{
ThreadPool.GetMaxThreads(out int maxThread, out int completionPortThreads);
ThreadPool.SetMinThreads(20, 20); // HERE <-------
stopwatch.Start();
var result = Parallel.For(0, 20, (i) =>
{
ThreadPool.QueueUserWorkItem(someThread, i);
});
while (!result.IsCompleted) { }
Console.WriteLine("Queueing completed...");
}
Results
...
7 started.
15 started.
9 started.
12 started.
17 started.
13 started.
16 started.
19 started.
18 started.
5 finnished.
3 finnished.
4 finnished.
6 finnished.
0 finnished.
14 finnished.
1 finnished.
10 finnished.
...
A nice clean devise.

Related

Task.Delay(millis) consistently blocks all threads for 100-500ms

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.

QueueUserWorkItem weird behavior

I'm using ThreadPool.QueueUserWorkItem() to start some background tasks.
The ThreadPool concurrency behavior is weird. My CPU has 4 logical cores. I expect that there are only 4 running threads. However, the sample code shows different behavior.
in time 1/2/3, Why does mores threads are triggered?
here is sample code:
class Program
{
static DateTime s_startTime = new DateTime();
public static void Main()
{
// Queue the task.
s_startTime = DateTime.Now;
for (int i = 0; i < 1000; i++)
{
ThreadPool.QueueUserWorkItem(ThreadProc, i);
}
Console.WriteLine("Main thread does some work, then sleeps.");
Thread.Sleep(100 * 1000);
Console.WriteLine("Main thread exits.");
}
// This thread procedure performs the task.
static void ThreadProc(Object i)
{
DateTime thread_starttime = DateTime.Now;
int a = Convert.ToInt32(i);
double ss = (thread_starttime - s_startTime).TotalSeconds;
Console.WriteLine("time:" + ss + ", start " + a);
Thread.Sleep(10 * 1000);
DateTime e = DateTime.Now;
double ee = (e - s_startTime).TotalSeconds;
Console.WriteLine("time:" + ee + ", end " + a);
}
}
output
Main thread does some work, then sleeps.
time:0.0040027, start 0
time:0.0360007, start 3
time:0.0360007, start 1
time:0.0360007, start 2
time:1.0178537, start 4
time:2.0191713, start 5
time:3.019311, start 6
time:4.0194503, start 7
time:5.0195775, start 8
time:6.0195875, start 9
time:7.0219127, start 10
time:8.0214611, start 11
time:9.0181507, start 12
time:10.020686, end 0
time:10.020686, start 13
time:10.020686, start 14
time:10.038517, end 1
time:10.038517, start 15
time:10.038517, end 3
time:10.0403473, start 16
time:10.038517, end 2
time:10.0413736, start 17
time:11.0233302, end 4
time:11.0243333, start 18
time:11.0243333, start 19
More threads will be triggered because you aren’t keeping your cores busy.
Thread.Sleep allows the thread to yield. Since your workers are mostly sleeping and not CPU bound the scheduler is free to schedule more threads.

Does ConfigureAwait affect non-threadpool threads only?

I am playing a little bit with ConfigureAwait because I would like to understand how it works.
Therfore, I wrote the following small console application (actually running in LINQPad):
void Main()
{
// Use dispatcher to execute the code on STA thread
Dispatcher.CurrentDispatcher.Invoke(() => Run());
}
private async static Task Run()
{
var continueOnCapturedContext1 = true;
var continueOnCapturedContext2 = true;
PrintThreadID();
await Task.Run(() => PrintThreadID()).ConfigureAwait(continueOnCapturedContext1);
PrintThreadID();
await Task.Run(() => PrintThreadID()).ConfigureAwait(continueOnCapturedContext2);
PrintThreadID();
}
private static void PrintThreadID()
{
Console.Write(Thread.CurrentThread.ManagedThreadId.ToString("00") + "\t");
}
And I got the following output:
A) true/true
var continueOnCapturedContext1 = true;
var continueOnCapturedContext2 = true;
1) 11 19 11 07 11
2) 11 09 11 12 11
3) 11 06 11 06 11
Expected: dispatcher thread (11) was captured and awaitable tasks were executed on different or same threadpool threads.
B) false/false
var continueOnCapturedContext1 = false;
var continueOnCapturedContext2 = false;
1) 11 23 23 22 22
2) 11 19 19 19 19
3) 11 10 10 10 10
Also expected: SynchronizationContext was not captured, so subsequent awaitable and non-awaitable code were run on threadpool thread (usually the same).
C) false/true
var continueOnCapturedContext1 = false;
var continueOnCapturedContext2 = true;
1) 11 14 14 06 06
2) 11 20 20 20 20
3) 11 17 17 08 08
The result of output 1 and 3 is strange. The 2. awaitbale task was executed with option "continue on captured context" so I would expect that it runs on the same thread as the code that called it.
It seems, that ConfigureAwait(true/false) has no effect on subsequent awaitable calls if it was alreay called before, right?
The 2. awaitbale task was executed with option "continue on captured context" so I would expect that it runs on the same thread as the code that called it.
That assumes that "context == thread", but it doesn't. The synchronization context that uses the thread-pool will resume on any thread in the thread-pool. Now if you don't capture the synchronization context, you'll still end up on "a thread in the thread pool".
So yes, if you're already on a thread-pool thread, it won't matter whether or not you capture the synchronization context... the continuation will still end up on a thread pool thread. But it's worth pointing out that it would be entirely reasonable to have a different synchronization context with multiple threads, and capturing the synchronization context would return to one of those threads, but not necessarily the same one.
It seems, that ConfigureAwait(true/false) has no effect on subsequent awaitable calls if it was already called before, right?
Not quite. That will be the case if the task needs to use the continuation. If the first task you call ConfigureAwait on has already completed, the code will continue to execute synchronously - so at that point, the second ConfigureAwait is important.
Example:
using System;
using System.Windows.Forms;
using System.Threading;
using System.Threading.Tasks;
class Test
{
static void Main()
{
var form = new Form();
form.Load += async (sender, args) =>
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
await Task.FromResult(10).ConfigureAwait(false);
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
await Task.Delay(1000).ConfigureAwait(false);
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
};
Application.Run(form);
}
}
Sample output:
1
1
5
So the second Console.WriteLine showed that the code was still running on the same thread despite the ConfigureAwait(false), because Task.FromResult returns an already-completed task.

Parallel Library: does a delay on one degree of parallelism delay all of them?

I have a ConcurrentBag urls whose items are being processed in parallel (nothing is being written back to the collection):
urls.AsParallel<UrlInfo>().WithDegreeOfParallelism(17).ForAll( item =>
UrlInfo info = MakeSynchronousWebRequest(item);
(myProgress as IProgress<UrlInfo>).Report(info);
});
I have the timeout set to 30 seconds in the web request. When a url that is very slow to respond is encountered, all of the parallel processing grinds to a halt. Is this expected behavior, or should I be searching out some problem in my code?
Here's the progress :
myProgress = new Progress<UrlInfo>( info =>
{
Action action = () =>
{
Interlocked.Increment(ref itested);
if (info.status == UrlInfo.UrlStatusCode.dead)
{
Interlocked.Increment(ref idead);
this.BadUrls.Add(info);
}
dead.Content = idead.ToString();
tested.Content = itested.ToString();
};
try
{
Dispatcher.BeginInvoke(action);
}
catch (Exception ex)
{
}
});
It's the expected behavior. AsParallel doesn't return until all the operations are finished. Since you're making synchronous requests, you've got to wait until your slowest one is finished. However note that even if you've got one really slow task hogging up a thread, the scheduler continues to schedule new tasks as old ones finish on the remaining threads.
Here's a rather instructive example. It creates 101 tasks. The first task hogs one thread for 5000 ms, the 100 others churn on the remaining 20 threads for 1000 ms each. So it schedules 20 of those tasks and they run for one second each, going through that cycle 5 times to get through all 100 tasks, for a total of 5000 ms. However if you change the 101 to 102, that means you've got 101 tasks churning on the 20 threads, which will end up taking 6000 ms; that 101th task just didn't have a thread to churn on until the 5 sec mark. If you change the 101 to, say, 2, you note it still takes 5000 ms because you have to wait for the slow task to complete.
static void Main()
{
ThreadPool.SetMinThreads(21, 21);
var sw = new Stopwatch();
sw.Start();
Enumerable.Range(0, 101).AsParallel().WithDegreeOfParallelism(21).ForAll(i => Thread.Sleep(i==0?5000:1000));
Console.WriteLine(sw.ElapsedMilliseconds);
}

Using async/await for multiple tasks

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.

Categories