6 ways to use parallelism with Tasks and Threads in C# .NET

Saulo da Cruz
4 min readFeb 27, 2024

--

At any moment you Developer will fall into some situation where you will need to use parallelism, whether to increase the performance of a service, a job, whatever be the goal of your program. But always when we need to decrease the time of a process, we think in to separate into parts that process and execute it in the same moment. At that moment we remember of parallelism, but we faced with question: What’s a better ways to implement it?

What will you learn in this article?

How to implement parallelism in C# .NET with Threads and Tasks using in some examples the class from .NET called by SemaphoreSlim. In this class we will help to apply a concept of Semaphore.

Threads vs Tasks which better?

Well, the evidence used in this article shows the implementation using Threads with considerably less execution time.

1 – Using Threads

In this example, we are adding in a list each thread and in the final we are waiting for all threads to finish. Also we are putting the variable count in the thread safe using the lock and we are using the anyObject auxiliary to ensure that all code into lock (anyObject) stays safe without corrupting the final result. The execution time spent in that implementation was 2 seconds, it was better compared to others below.

var count = 0;
var anyObject = new object();
UseThreads();

void UseThreads()
{
var threads = new List<Thread>();

for (int i = 0; i < 100; i++)
{
var thread = new Thread(() =>
{
AnyMethod();
lock (anyObject) count++;
});

thread.Start();
threads.Add(thread);
}

threads.ForEach(t => t.Join());
}

void AnyMethod() => Thread.Sleep(1000);

//Time spent: 2 seconds

2 – Using Tasks

In this example, how to give example above, we are adding in a list each task and in the final we are waiting for all tasks to finish. Also we are putting the variable count in the thread safe using the lock and we are using the anyObject auxiliary to ensure that all code into lock (anyObject) stays safe without corrupting the final result. The execution time spent in that implementation was 11 seconds, compared with the first can we see a difference of 9 seconds more. By far not the best choice.

var count = 0;
var anyObject = new object();
UseTasks();

void UseTasks()
{
var tasks = new List<Task>();

for (int i = 0; i < 100; i++)
{
var task = new Task(() =>
{
AnyMethod();
lock (anyObject) count++;
});

task.Start();
tasks.Add(task);
}

Task.WaitAll(tasks.ToArray());
}

void AnyMethod() => Thread.Sleep(1000);

//Time spent: 11 seconds

3 – Using Parallel For

In this case the gain was the reduction lines codes what contributed with clean code. But the execution time spent in that implementation was 12 seconds.

var count = 0;
var anyObject = new object();
UseParallelFor();

void UseParallelFor()
{
Parallel.For(0, 100, (i) =>
{
AnyMethod();
lock (anyObject) count++;
});
}

void AnyMethod() => Thread.Sleep(1000);

//Time spent: 12 seconds

4 – Using Parallel ForEach

In this example, follow the example above, the difference is that’s follow the same idea from ForEach and the execution time spent was the same 12 seconds.

var count = 0;
var anyObject = new object();
UseParallelForEach();

void UseParallelForEach()
{
Parallel.ForEach(new int[100], (i) =>
{
AnyMethod();
lock (anyObject) count++;
});
}

void AnyMethod() => Thread.Sleep(1000);

//Time spent: 12 seconds

5 – Using Semaphore with Threads

Here we used the semaphore concept, where we defined the threads number that will be used in class SemaphoreSlim. How to parameter we get the number of logical processors that contain in my processor. For example: if your processor has 8 logic processors, you will have 8 threads, but if you want more threads than the quantity of logical processors from you processor, you can lose the power of processing in each thread.
With this implementation, we spent 13 seconds in execution time. We will remember that we execute the same AnyMethod() function.

var count = 0;
var anyObject = new object();
UseSemaphoreWithThreads();

void UseSemaphoreWithThreads()
{
var semaphoreSlim = new SemaphoreSlim(Environment.ProcessorCount);
var threads = new List<Thread>();

for (int i = 0; i < 100; i++)
{
var thread = new Thread(() =>
{
semaphoreSlim.Wait();

AnyMethod();
lock (anyObject) count++;

semaphoreSlim.Release();
});

thread.Start();
threads.Add(thread);
}

threads.ForEach(t => t.Join());
}

void AnyMethod() => Thread.Sleep(1000);

//Time spent: 13 seconds

6 – Using Semaphore with Tasks

Here we used the same idea as above, but using Tasks and the result was the same, 13 seconds.

var count = 0;
var anyObject = new object();
UseSemaphoreWithTasks();

void UseSemaphoreWithTasks()
{
var semaphoreSlim = new SemaphoreSlim(Environment.ProcessorCount);
var tasks = new List<Task>();

for (int i = 0; i < 100; i++)
{
var task = new Task(() =>
{
semaphoreSlim.Wait();

AnyMethod();
lock (anyObject) count++;

semaphoreSlim.Release();
});

task.Start();
tasks.Add(task);
}

Task.WaitAll(tasks.ToArray());
}

void AnyMethod() => Thread.Sleep(1000);

//Time spent: 13 seconds

About Thread Safe

When we use threads, we can run some risks with the variables that are outer context from each thread, how to it count for example, can there be changes in the result and won’t given an exact result, what is not interesting for us. To solve this problem, we can use the lock and keep our result safe. Below is an example of count, if I did not have implemented lock (anyObject) the count cannot have its result exactly because of the competition between threads.

lock (anyObject) count++; 

// Or you can use "Interlocked.Increment(ref count);" but you cannot
// use in this case "count++;" directly because it is not thread safe
// and the value of count will be wrong.

Conclusion

These options, the better performance was using Threads executing in 2 seconds, but the better implementation was examples using Parallel, had the less code lines favoring the clean code. Well you know the better choice for your problem.

Thanks for reading and I hope to helped.

--

--