This repository's main purpose is to demonstrate the various tools that the .NET ecosystem provides us to write code that can run in parallel. Feel free to contribute. :)
- Required environment
- Demo Application
2.1) Static File server
2.2) Throttled Downloader library
2.3) Benchmark tool - Instructions for running
3.1) Benchmark
3.2) Debug - Demonstrated tools
4.1) Baselines
4.2) Low level abstractions
4.3) Mid level abstractions
4.4) High level abstractions - Sample benchmark result
- .NET Profiling
- Known missing sample codes
- .NET Core 3.0
- Visual Studio 2019
In order to demonstrate the different capabilities, we need a demo application.
In our case this app will be a throttled parallel downloader.
The solution contains three projects:
It is an ASP.NET Core 3.0 web-application which can serve static files for http clients.
It exposes the files under the Resources folder through the /resources route.
Related project: ThrottledParallelism
It is a .NET Core 3.0 library, which is exposing a simple API and several implementations of it.
public interface IGovernedParallelDownloader
{
Task DownloadThemAllAsync(IEnumerable<Uri> uris, ProcessResult processResult, byte maxThreads);
}
Related project: LogFileServer
It is a .NET Core 3.0 console application, which is used to perform micro-benchmarking.
It measures execution time, GC cycles, etc.
Related project: RunEverythingInParallel
NOTE
Please note that this demo is I/O bound.
Which means that using techniques like Task.Run
or Parallel.XYZ
, which are CPU-bound, does not make too much sense, because they are limited to the number of cores in the machine.
So, please scrutinize the provided examples with this in mind.
- Make sure that Program.cs of the RunEverythingInParallel project look like this:
using System;
using System.Threading;
using BenchmarkDotNet.Running; //BenchmarkRunner
namespace RunEverythingInParallel
{
class Program
{
//Use Release
static void Main(string[] args)
{
Thread.Sleep(1000); //Wait for the WebApp to start
BenchmarkRunner.Run<ThrottledDownloader>(); //Add as many benchmarks as you want to run
Console.ReadLine();
}
}
}
- Build the solution in Release mode (Set the Optimize node in the csproj to true if it is needed)
- Hit Ctrl+F5 in Visual Studio
- If you want to run it without VS (by using the dotnet cli) then run the LogFileServer project prior the RunEverythingInParallel
- Make sure that Program.cs of the RunEverythingInParallel project look like this:
using System;
using System.Threading;
using ThrottledParallelism.Strategies;
namespace RunEverythingInParallel
{
class Program
{
//Use Debug
static void Main(string[] args)
{
Thread.Sleep(1000); //Wait for the WebApp to start
var downloader = new ThrottledDownloader();
downloader.Setup();
downloader.RunExperiment<HighLevel_Foreach_AsParallel>();
}
}
}
- Build the solution in Debug mode (Set the Optimize node in the csproj to false if it is needed)
- Hit F5 in Visual Studio
- Analyze the choosen code by using the Parallel Watch, Parallel Stack and Tasks windows
No. | Channel | Synchronizer | Workers via | Throttled by | File |
---|---|---|---|---|---|
1 | IEnumerable | - | main thread | - | Link |
2 | IEnumerable | Task.WhenAll | Task | - | Link |
No. | Channel | Synchronizer | Workers via | Throttled by | File |
---|---|---|---|---|---|
1 | BlockingCollection | AsyncCountdownEvent | ThreadPool.QueueUserWorkItem | Manually (for (i = 0; i < maxThreads; i++)) | Link |
2 | BlockingCollection | CountdownEvent | ThreadPool.QueueUserWorkItem | Manually (for (i = 0; i < maxThreads; i++)) | Link |
3 | BlockingCollection | Parent Task | Children Tasks | Manually (for (i = 0; i < maxThreads; i++)) | Link |
4 | BlockingCollection | Task.WhenAll | Task | Manually (for (i = 0; i < maxThreads; i++)) | Link |
5 | IEnumerable<KeyValuePair<Uri, Func<Uri, Task>> | Task.WhenAll | Task | Load balancing by MoreLinq's Segment | Link |
6 | IGrouping<int, Job> | Task.WhenAll | Task | Load balancing by MoreLinq's GroupAdjacent | Link |
No. | Channel | Synchronizer | Workers via | Throttled by | File |
---|---|---|---|---|---|
1 | ActionBlock | CancellationTokenSource + Interlocked | Task | ExecutionDataflowBlockOptions | Link |
2 | ActionBlock + BatchBlock | PropagateCompletion + Completion | Task | ExecutionDataflowBlockOptions | Link |
3 | BufferBlock | Task.WhenAll + ImmutableList | Task | Manually (for (i = 0; i < maxThreads; i++)) | Link |
4 | Channel | Task.WhenAll | Task | Manually (Enumerable.Range(0, maxThreads -1)) | Link |
No. | Channel | Synchronizer | Workers via | Throttled by | File |
---|---|---|---|---|---|
1 | Partitioner | Parallel.Foreach | Task + AsyncHelper.RunSync!!! | ParallelOptions | Link |
2 | IGrouping<int, Uri> | Parallel.Invoke | Task + AsyncHelper.RunSync!!! | GroupBy (round robin) | Link |
3 | IGrouping<int, Uri> | Parallel.For +TLS | Task + AsyncHelper.RunSync!!! | GroupBy + Parallel.For | Link |
4 | ConcurrentQueue | Task.WhenAll | Task | Manually (Enumerable.Range(0, maxThreads -1)) | Link |
5 | IEnumerable | ParallelForEachAsync | Task | ParalellelForEachAsync | Link |
6 | HashSet | Task.WhenAny | Task | Manually only during initialization | Link |
7 | IAsyncEnumerable | await last Task | Task | SemaphoreSlim | Link |
8 | ParallelQuery | Task.WhenAll | Task | WithDegreeOfParallelism | Link |
9 | ParallelQuery | Custom Awaiter | Task | WithDegreeOfParallelism | Link |
10 | IEnumerable | Task.WhenAll | Task | SemaphoreSlim | Link |
11 | IEnumerable | Task.WhenAll | Task | BulkheadAsync | Link |
BenchmarkDotNet=v0.11.5
OS=Windows 10.0.17134.1069 (1803/April2018Update/Redstone4)
Intel Core i7-8650U CPU 1.90GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores Frequency=2062501 Hz, Resolution=484.8482 ns
.NET Core SDK=3.0.100
Host : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), 64bit RyuJIT
IterationCount=3 RunStrategy=ColdStart
Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated |
---|---|---|---|---|---|---|---|---|---|
BaseLine_Sequentially | 3.151 s | 1.3267 s | 0.0727 s | 1.00 | 0.00 | 377000.0000 | 47000.0000 | 38000.0000 | 1600419.34 KB |
BaseLine_EmbarrassinglyParallel | 1.055 s | 1.8874 s | 0.1035 s | 0.33 | 0.03 | 270000.0000 | 1000.0000 | 1000.0000 | 58.29 KB |
CSharp3 | 1.426 s | 0.7124 s | 0.0390 s | 0.45 | 0.02 | 320000.0000 | 8000.0000 | 7000.0000 | 1.98 KB |
CSharp5 | 1.943 s | 1.6989 s | 0.0931 s | 0.62 | 0.02 | 383000.0000 | 4000.0000 | 4000.0000 | 4.32 KB |
CSharp6 | 1.318 s | 0.6582 s | 0.0361 s | 0.42 | 0.00 | 367000.0000 | 1000.0000 | 1000.0000 | 22.23 KB |
CSharp8 | 1.340 s | 1.8518 s | 0.1015 s | 0.42 | 0.02 | 300000.0000 | 4000.0000 | 4000.0000 | 13.41 KB |
Bonus | 1.999 s | 2.2747 s | 0.1247 s | 0.63 | 0.04 | 405000.0000 | 3000.0000 | 3000.0000 | 30.4 KB |
If you want to deep dive into the execution details, I highly recommend you to use some profiling.
If sampling is enough for you, then I encourage you to use CodeTrack
If tracing is needed, then you can play with the Concurrency Visualizer step-by-step
- Reactive eXtensions
- ideas are more than welcome