Multithreading in C# (.NET)
Translated from German using DeepL.
Date: June 2022
Reading time: 12 minutes
In this blog post, I describe the concept of multitasking. After explaining multithreading, I will go into the thread and TPL classes of C#.
Process
First of all, you need to understand what a process is.
A process is nothing more than the execution of a program. A program usually receives an input, performs a calculation and delivers an output.
an output. Programs always run on operating systems and can be seen in the task manager under Windows.
Thread
A process consists of at least one thread.
A thread is the executable unit of a process. Threads work on a task independently of other parts of the process.
Threads can be prioritized, are scheduled and can apparently be executed in parallel.
Task manager
In the task manager you can view information about processes, threads, processors, cores and more.
Core: The cores are the brains of the computer. They can execute programs. Most laptops today have four, six or eight cores.
Multitasking
Multitasking means that several processes are processed simultaneously.
However, this is not true, as a processor can only process one program at a time. The CPU switches back and forth between the programs so that it appears as if the processes are being processed simultaneously. This makes use of a mechanism that allocates time.
CPU: Is a primary component of every computer. The CPU acts as the control center.
Preemptive multitasking
This type of multitasking is used by newer operating systems. It is based on priorities. The operating system has full control over the release of resources. The kernel allocates a short period of time to each application program in turn. short period of time to each application program.
Kernel: The kernel is the interface between the software and hardware. It monitors processes.
Cooperative multitasking
This is a type in which the processes control the resources themselves. This type was replaced because malicious software was able to abuse this principle. could abuse this principle.
What is multithreading?
Imagine that you are writing an application and sending it by e-mail. Since such an e-mail has several attachments, sending it can take a while.
take a while.
Multithreading makes it possible that you do not have to wait while sending. This allows you to continue working directly with the application while the mail is
the mail is sent "simultaneously" in the background.
With multithreading, the processes are divided into smaller pieces. These sub-processes, known as threads, ensure more efficient processing through "parallel" execution.
for more efficient processing.
As already described, the execution only works in parallel due to multitasking.
This concept can increase the processing speed. However, caution is advised. This is because threads can influence each other.
Many of the IT concepts originate from real life. For this reason, threads can also be explained using a simple example. example.
School, work at Merkle and leisure time could each be seen as a process.
The main thread at school is lessons. Individual tasks (school subjects) are worked through in sequence.
A secondary thread is learning. This is also part of school, but is separate from the main thread. In learning, there are again individual tasks. In this
In this case, these are learning blocks for upcoming exams. These tasks are prioritized. For exams that are closer in the future or are a little more difficult
difficult, you should start studying earlier. This is why they are further forward in the queue.
You work asynchronously when studying. This is because you often start learning physics before you have completely finished PHP.
If you learn early enough, you can store the knowledge in your long-term memory (hard drive). If you only start shortly before, things are often only stored in
short-term memory (memory).
Threads can also share something among themselves. Because the knowledge I acquire at home can also be reused in other threads.
Deadlocks
Two cooks in a kitchen are given the task of cooking a dish. They need a pan and a bottle of oil. Both cooks rush off to
to get the resources.
Cook 1 was able to get the pan. Cook 2 holds the oil. As each cook now has a resource, they find themselves in a tricky situation. None of the
cooks want to give up their pan or their oil. However, they both need the other's resource to prepare the dish.
This results in a deadlock.
Deadlocks therefore describe the situation in which processes are blocked. Both processes hold a resource that they do not want to release for others while they are simultaneously waiting for another resource (which is required by another process). The situation can be resolved by determining how the program should proceed.
Thread class
From the very beginning, .NET offered a way to develop multithreaded applications. They provided the Thread class for this purpose.
Each thread can be in one of three possible states:
- waiting
- ready
- running
Start
ThreadStart del = new ThreadStart(TestMethod);
Thread thread = new Thread(del);
thread.Start();
Procedure
class Program {
static void Main(string[] args) {
ThreadStart del;
del = new ThreadStart(TestMethod);
Thread thread = new Thread(del);
// start second thread
thread.Start();
for(int i = 0; i <= 100; i++) {
for(int k = 1; k <= 20; k++) {
Console.Write(".");
}
Console.WriteLine("Primär-Thread " + i);
}
Console.ReadLine();
}
// method is executed in its own thread
public static void TestMethod() {
for(int i = 0; i <= 100; i++) {
for(int k = 1; k <= 20; k++) {
Console.Write("X");
}
Console.WriteLine("Sekundär-Thread " + i);
}
}
}
As you can see in the output, the threads are executed alternately.
Exit
Threads can also be closed again.
thread.Abort();
Dependencies
Threads may be dependent on each other. Join
can block the called thread until the secondary thread is completely terminated.
Priorities
As already mentioned, you can assign priorities to threads.
class Program {
static void Main(string[] args) {
Demo obj = new Demo();
Thread thread1, thread2;
thread1 = new Thread(new ThreadStart(obj.Execution1));
thread2 = new Thread(new ThreadStart(obj.Execution2));
thread1.Priority = ThreadPriority.AboveNormal;
thread1.Start();
thread2.Start();
Console.ReadLine();
}
}
class Demo {
public void Execution1() {
for (int i = 0; i <= 500; i++) {
Console.Write(".");
}
}
public void Execution2() {
for (int number = 0; number <= 10; number++) {
Console.WriteLine("It's me, Thread2");
}
}
}
In this example, the priority in line nine is set to AboveNormal
.
Thread pools
Thread pools simplify working with threads. A certain number of threads are created at startup. These can then be used
be used. The advantage of this is that you do not have to create your own threads.
If a thread method is terminated, the free thread is returned to the pool and is therefore available for other tasks.
class Program {
static void Main(string[] args) {
// explore thread pool
int asyncThreads;
ThreadPool.GetMaxThreads(out maxThreads, out asyncThreads);
Console.WriteLine("max. threads amount: {0}", maxThreads);
Console.WriteLine("max. e/a-threads amount: {0}", asyncThreads);
Console.WriteLine(new string('-', 40));
// notification event, status 'do not signal'
AutoResetEvent ready = new AutoResetEvent(false);
// requesting a thread from the pool
ThreadPool.QueueUserWorkItem(new WaitCallback(Calculate), ready);
Console.WriteLine("The main thread is waiting ...");
// set main thread to wait state
ready.WaitOne();
Console.WriteLine("secondary thread is done.");
Console.ReadLine();
}
public static void Calculate(object obj) {
Console.WriteLine("in secondary thread");
Thread.Sleep(5000);
// set event status to 'signal'
((AutoResetEvent)obj).Set();
}
}
Synchronization
An invalid state could occur if the system deprives a thread of time in the middle of execution and another thread is working on the same object.
working on the same object.
This application shows the problem.
class Program {
static void Main(string[] args) {
Demo obj = new Demo();
Thread thread1, thread2;
thread1 = new Thread(new ThreadStart(obj.Worker));
thread2 = new Thread(new ThreadStart(obj.Worker));
thread1.Start();
thread2.Start();
Console.ReadLine();
}
}
class Demo {
private int value;
public void Worker() {
while(true) {
value++;
if (value > 100) break;
Console.WriteLine(value);
}
}
}
85
63
87
88
89
90
91
92
93
94
95
96
86
98
99
100
97
Thread safety allows multiple threads to call the same method at the same time without conflicts arising.
The monitor can be used for this purpose. This prevents several threads from running through a certain part of the program at the same time. The
class provides an Enter
and Exit
method with which the critical sections can be defined.
private int value;
public void Worker() {
while(true) {
Monitor.Enter(this); // set lock
value++;
if (value > 100) break;
Console.WriteLine("number = {0,5} thread = {1,3}", value,
Thread.CurrentThread.GetHashCode().ToString());
Thread.Sleep(5);
Monitor.Exit(this); // unlock
}
}
To find out more exciting things about this topic, I can recommend the book https://openbook.rheinwerk-verlag.de/visual_csharp_2012/1997_15_002.html#dodtp4d7cb142-5b23-44cf-8fa5-93084ae61382 (opens in a new tab). It also deals with Lock
, Wait & Pulse
and Mutext
.
Task Parallel Library (TPL)
Working with the complex thread class can sometimes be a little difficult. That is why there is a more abstract variant.
The TPL means that developers no longer have to worry about each individual thread. The task is in focus. Threads are then created
created automatically at runtime.
Another advantage is that it enables the use of multiprocessors. A computer can divide up several processes.
It is important to note that the performance curve is not proportional to the number of processors. The computing power is also dependent on
the intelligence of the operating system. Under certain circumstances, the application can even become slower, as the distribution across several processor cores
is associated with effort. It is therefore important to weigh things up. The more processor cores can be used, the more time-intensive the operations should be.
should be.
The class can be found under the following namespace:
System.Threading.Tasks (& System.Collections.Concurrent)
.
Parallel
Three simple methods are provided for the parallelization of code and loops.
Invoke
Invoke
blocks the program flow and defines which methods are to be processed in parallel.
class Program {
static void Main(string[] args) {
Parallel.Invoke(Task1, Task2, Task3);
Console.ReadLine();
}
static void Task1() {
for (int i = 0; i < 10; i++) {
Thread.Sleep(50);
Console.Write(" #1 ");
}
}
static void Task2() {
for (int i = 0; i < 10; i++) {
Thread.Sleep(50);
Console.Write(" #2 ");
}
}
static void Task3() {
for (int i = 0; i < 10; i++) {
Thread.Sleep(50);
Console.Write(" #3 ");
}
}
}
For
With a high number of loop passes, parallelization with For
can improve performance.
class Program {
static void Main(string[] args) {
Stopwatch watch = new Stopwatch();
watch.Start();
ParallelTest();
watch.Stop();
Console.WriteLine(watch.ElapsedMilliseconds);
watch.Reset();
watch.Start();
SynchTest();
watch.Stop();
Console.WriteLine(watch.ElapsedMilliseconds);
Console.ReadLine();
}
static void SynchTest() {
double[] arr = new double[1000000];
for(int i = 0; i < 1000000; i++) {
arr[i] = Math.Pow(i, 0.333) * Math.Sqrt(Math.Sin(i));
}
}
static void ParallelTest() {
double[] arr = new double[1000000];
Parallel.For(0, 1000000, i => {
arr[i] = Math.Pow(i, 0.333) * Math.Sqrt(Math.Sin(i));
});
}
}
In this example with one million runs, the method runs approx. 50% faster. However, the lower this number is, the lower the advantage.
Interruptions
Loops can also be interrupted at any time.
Parallel.For(0, 1000000, (i, option) => {
arr[i] = Math.Pow(i, 0.333) * Math.Sqrt(Math.Sin(i));
if (i > 1000) option.Stop();
});
Foreach
Again, in my opinion, this method is quite understandable.
string[] namen = { "Peter", "Uwe", "Udo", "Willi", "Pia", "Michael", "Conie" };
Parallel.ForEach(namen, name => {
Console.WriteLine(name);
});
Task
The TPL provides a Task
class, which is very similar to the Thread class. There are two variants for defining a task.
Task task1 = Task.Factory.StartNew(DoSomething);
Task task2 = new Task(Test);
task2.Start();
Lambda
A task can also be defined using lambda syntax.
Task task1 = Task.Factory.StartNew(() => {
Console.WriteLine("Task is executed...");
})
Wait
It is possible to wait until a task is finished.
Task task = Task.Factory.StartNew(() => {
Console.WriteLine("Long operations...");
Thread.Sleep(5000);
Console.WriteLine("Done...");
});
task.Wait();
Console.WriteLine("Task is done...");
WaitAny
With WaitAny
you can wait until a task from a list of tasks has been executed.
WaitAll
This waits for the completion of all tasks.
Task task1 = Task.Factory.StartNew(() => {
Thread.Sleep(10000);
Console.WriteLine("Task #1: done...");
});
Task task2 = Task.Factory.StartNew(() => {
Thread.Sleep(3000);
Console.WriteLine("Task #2: done...");
});
Task task3 = Task.Factory.StartNew(() => {
Thread.Sleep(6000);
Console.WriteLine("Task #3: done...");
});
Task.WaitAll(task1, task2, task3);
Task
Tasks can also return something. To use return values, you must use the Task<TResult>
class.
int value = 12;
Task<long> task = Task<long>.Factory.StartNew((v) => {
int var = (int)v;
Thread.Sleep(3000);
return var * var;
}, value);
Console.WriteLine("I wait...");
Console.WriteLine("Result: {0}", task.Result);
CancellationTokenSource
An operation can be canceled externally with the help of a token.
class Program {
static void Main(string[] args) {
var cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
Task task = Task.Factory.StartNew(() => {
Thread.Sleep(1000);
while(true) {
if (token.IsCancellationRequested) {
token.ThrowIfCancellationRequested();
}
}
}, cts.Token);
cts.Cancel();
Console.WriteLine("Aborting the parallel operation...");
try {
task.Wait();
} catch (Exception ex) {
Console.WriteLine("In catch: " + ex.InnerException.Message);
}
Console.ReadLine();
}
}
Async & Await
Async
enables the definition of methods that do not block. With Await
you wait until the task is finished.
During the execution of such methods, the caller can continue his work.
This is often used when making API calls, writing or reading files or when loading media.
class Program {
static void Main(string[] args) {
StartAsyncMethod();
for (int i = 0; i < 1000; i++) {
Console.Write(".");
Console.ReadLine();
}
}
static async void StartAsyncMethod() {
Console.Write("Start");
Console.Write(await AsyncMethod());
}
static async Task<string> AsyncMethod() {
await Task.Delay(20);
return "Done";
}
}
Producer Consumer
If you have two processes, process A cannot access B's memory. A third system or a queue is therefore required.
A queue is a waiting list. This can take the form of memory, a TXT file or a database.
The producer now adds an event to this queue. Meanwhile, a consumer waits for work from the queue and executes it according to the FIFO
(First In First Out) principle.
Conclusion
Here are the most important points in brief.
A process is a program that can be executed on an operating system. A thread is an executable unit in a process.
Multithreading makes it possible not to have to wait while a task is being processed. In C#, there are two ways to implement multithreading
multithreading.
The thread class is close to the program code. However, it is also somewhat more complex. The more abstract variant is the Task Parallel Library (TPL).
Nowadays, this is mostly used.
The producer/consumer pattern solves the problem that two processes cannot use the same data. This is handled with the help of a third system
or a queue.
Personally, I first came into contact with the topic of threads in a course on operating systems. Now I was able to learn a lot more
learn.
In my opinion, it makes sense to get to grips with the subject. I now have a better understanding of what happens in the background of a program
and how I can influence this if necessary. I can imagine that I will need the Async & Await definitions more often.
will be needed more often.
Some code examples come from the Visual C# 2012 book (opens in a new tab).