Lesson 1 - Introduction to multi-threaded applications in C#.NET
Welcome to the first lesson about programming multi-threaded applications in
C# .NET. We're going to learn how to use modern multi-core processors
efficiently, and also how to run tasks in the background, preventing the main
application thread from freezing. We'll get to the latest technologies from the
.NET framework such as parallel programming, Tasks or the async
and
await
keywords.
Introduction to the thread theory
Before we start using threads, let's describe how our operating system runs applications. Since we're learning C#, we can talk about Windows, but the principle can be applied for most other operating systems. Windows is a multi-tasking operating system. This means that it can run multiple applications at once. Nevertheless a processor core can usually run only one instruction at one time. You know that Windows was able to run multiple applications at a time long before multi-core processors were even on the market, and now you can also run as many applications as you want, no matter how many cores your PC's processor has. How is it possible?
Windows is simply switching between the running applications very quickly (more precisely, this is done by the scheduler, and we refer to the switching as to time-slicing). The system stops one application and starts another one for a moment. The user sees it as all the applications were running at once, even though they do not. With multi-core processors, this principle hasn't changed, Windows still switches between applications but can execute multiple instructions at the same time on each processor core.
Application, Process, and Thread
Let's think about the terms application, process, and thread. We can certainly imagine an application, it is, for example, this web browser in which you're reading this article. However, what's a process and what's a thread?
Process
A process is an instance of a running application. If we run a calculator
three times, we can find the calc.exe
process three times in the
task manager. Some more complex applications can use several processes, e.g.
Google Chrome creates a process for each open tab. However, in most cases, each
application has only one process.
Thread
There can be multiple threads running in one process. Each application has at least one process in which the main thread or, eventually, some other threads run. While the main thread is created for us automatically, we create other threads on our own. The operating system runs our thread on a core, then quickly puts it to sleep and wakes it up again as it needs. As a result, the threads in the process run in parallel and we can, for example, perform complex analyses without blocking the main application thread and even using several threads at a time, while the calculation itself will be several times faster.
Synchronization
Everything sounds great, doesn't it? However, in practice, threads are usually used only if we really need them. There's a huge problem with them - synchronization. We never know when our threads will be put to sleep. The system will do it no matter what the thread is doing. We can imagine that the thread method gets stuck on some line of the source code. In fact, however, it can also get stuck, for example, in the middle of the addition operation, because two instructions are needed to be executed to add 64-bit numbers on a 32-bit processor. If another thread is using this value, it may result in nonsense. Variables are also cached in the cores, so the same variable can have multiple different values at the same time. In this course, we'll get to the synchronization problems in detail and also learn how to solve them.
Threads are certainly not something that every application must necessarily have, no matter Microsoft engineers are trying their best to as easy to use as possible (and they are quite successful in it). The application still becomes more complicated with this technology. Be sure to use them with caution.
First multi-threaded application
Let's program our first multi-threaded application. For the sake of
simplicity, we'll only work in the console for a while. Create a new project and
name it Switcher
. We'll add a class of the same name to the project
with the following contents:
class Switcher { public void Write0() { while (true) { Console.Write("0"); } } public void Write1() { while (true) { Console.Write("1"); } } public void Switch() { Thread thread = new Thread(Write0); thread.Start(); Write1(); } }
The first two methods of the class are very simple and simulate a long activity. The first method prints zeroes to the console continuously, the second method prints ones in the same way.
The Switch()
method is more interesting for us. It calls the
method printing ones on its own. But before that, it creates a new thread and
assigns the method printing zeros to it. It also starts this new thread.
In .NET, threads are represented by the Thread
class. We pass
the ThreadStart
delegate to it through the constructor. It has the
following declaration:
public delegate void ThreadStart();
So, we can pass a parameter-less method of the void
type to a
thread. In the main()
method, we'll create a Switcher
instance and let it switch:
static void Main(string[] args) { Switcher switcher = new Switcher(); switcher.Switch(); }
When we run the app, we get this output:
Notice that the ones and zeroes are not switching equally as we might expect. We can see how each thread runs for a while and then is put to sleep. The intervals also differ, even though on average, both threads are running for the same amount of time.
Tread class properties
So far, we're interested in the following properties of the
Thread
class:
IsAlive
- Indicates whether the thread method is running.Name
- We can give a name to every thread, which makes debugging the application easier.
The main application thread
We get the main application thread using the static
Thread.CurrentThread
property. Unfortunately, no name is assigned
to it by default. Let's try to assign a name to it and print it:
static void Main(string[] args) { Thread.CurrentThread.Name = "Main thread"; Console.WriteLine(Thread.CurrentThread.Name); }
In the next lesson, Threads in C# .NET - Sleep, Join, and lock, we'll look at putting threads to sleep, joining them and we'll go through the basics of synchronization.