First of all: It depends. Multi-Threading offers great performance-boosts, but also occupies development and cpu-time to get everything working smooth. Very often adding multi-threading is just an attempt to compensate inefficent code.
Note, that just using async / await is NOT multithreading, it is just "unblocking" the current thread to be able to do something else. https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/task-asynchronous-programming-model
As a personal flavour, i'm using (real) threads for background-tasks, and whenever it comes down to adding multi-threading to a workflow that is overall sequential, with just parallel processing possibilities, Parallel.Foreach() is quite easy to use and can speed up processing of classic for/foreach loops.
In General, Parallel.Foreach() will process items one by one (or say 18 by 18) depending on the degree of parallelism configured. (I always use CPU-Count - 2):
List<Something> somethingList;
int availableCores = 20 - 2;
Parallel.ForEach(somethingList, new ParallelOptions { MaxDegreeOfParallelism = availableCores }, entry => {
//entry is now a single "Something" item
}
Most the time it is more convinient, if you split an existing list into chunks of equal size - and have 18 parallel executions processing one chunk.
Here is a little extension method (for lists) to achieve that:
public enum CollectionChunkStyle
{
MAX_LENGTH_PER_CHUNK,
FIXED_COUNT_OF_CHUNKS
}
public static List<List<T>> Chunk<T>(this List<T> list, int size, CollectionChunkStyle style)
{
List<List<T>> result = new List<List<T>>();
if (style == Enums.CollectionChunkStyle.MAX_LENGTH_PER_CHUNK)
{
List<T> chunk = new List<T>();
result.Add(chunk);
int c = 0;
foreach (T t in list)
{
if (c++ == size)
{
chunk = new List<T>();
result.Add(chunk);
c = 1;
}
chunk.Add(t);
}
}
else if (style == Enums.CollectionChunkStyle.FIXED_COUNT_OF_CHUNKS)
{
for (int i=0; i<Math.Min(size, list.Count); i++)
{
result.Add(new List<T>());
}
int c = 0;
foreach (T t in list)
{
result.ElementAt(c++ % size).Add(t);
}
}
return result;
}
It can either be used with FIXED_COUNT_OF_CHUNKS - so, say "Split this into 18 lists!" or with MAX_LENGTH_PER_CHUNK - saying "Create many chunks, 100 items per chunk please".
Above example then would be as easy as:
List<Something> somethingList;
int availableCores = 20 - 2;
List<List<Something>> chunks = somethingList.Chunk(availableCores, CollectionChunkStyle.FIXED_COUNT_OF_CHUNKS);
Parallel.ForEach(chunks , new ParallelOptions { MaxDegreeOfParallelism = availableCores }, chunk => {
//chunk is now a list with 1/18 of somethingLists elements.
}