Normally the programs that you write are single-threaded. By single-threaded, I mean that the processor starts at the first instruction in the program and then executes them one-by-one in the sequential manner, of course with some branching here and there, until eventually it reaches the last instruction in the program and the program ends.
But there are cases when you need to somehow do two or more pieces of work at the same time or do them independent of other pieces. Let’s suppose you are building a desktop application which has two buttons, Start
and Cancel
. Pressing on Start
would start some long running task and pressing Cancel
should cancel that long running task.
If you try to build the above application in the normal way (single-threaded way), you would see the main drawback of the single threaded model of execution. When the user presses on Start
, the long running task would start running and since the whole appplication is running in a sequential manner, your application freezes until that task is finished. And this means, user won’t be able to cancel the task by pressing Cancel
button.
This is one of the case, where threads can help you. But before understanding what threads are, you need to understand what is a Process
.
A Process
is an instance of your program while it is running. Your program instructions residing in some file on the hard disk doesn’t constitute a Process
. When that program is actually launched, then a Process
is created for your program by the operating system.
Process
mainly consists of following things
Without threads, Process
is the basic unit of execution that an operating system can schedule to run at a moment of time. When there are multiple Process
es, the operating system allocates each Process
just a quantum of time and keeps doing the switch from one Process
to the another. To make this switching possible, the operating system saves the current state of the Process
in an structure called Process Control Block
which has basically the same information described above. This enables to restore the state of the Process
when it is later rescheduled by the operating system and to resume from the same point where it left off.
Threads allows us to have multiple concurrent flow of execution in a single Process
. With threads, we have one more level of scheduling that the operating system can do. And that is, which thread out of all the threads in the Process
to execute at that moment.
When you launch a program, the operating system creates a Process
for that program with all the information as described above, and starts the execution of the instructions. This is the main thread of execution.
Now what you can do is, you can create a new thread in the same program. A thread will have following things,
As you can see, most of the things thread shares with the Process
. You can think of the Process
as the unit of resource allocation for a program and Thread
as the unit of execution. Process
has the resources such as memory, file descriptors and also other things such as code to be executed and permissions. All the threads that you will create will share these things with the Process
but will have some additional things of their own.
Each thread will have a local thread stack. Since each thread is a own flow of execution in a program, each thread needs its own stack to keep track of the call stack.
Each thread also needs to store its state. State includes the contents of the registers and the program counter when that thread was put in paused state.
If you are familiar with the fork
function in C
language, threads are not the same things. fork
command is used to create child Process
es of the current Process
. The things that you can do with threads, you can also do them with these child Process
es, but creating these child Process
es and doing context switching between them is way more expensive than creating threads and doing context switching among threads.
The reason for this is very simple -
Process
, whole copy of the parent Process
is created, and that includes the file descriptors, permissions, code to be executed, etc. The child Process
is a whole life bearing Process
in itself, like it’s parent Processs
. But while creating a thread, since each thread shares most of the things with the Process
, it needs to create only the local thread stack and the state, which is very cheap.Process
takes much more time as compared to context switch on a Thread. The reason being, process has to save more information as compared to the thread.Enough with the theory. Let’s see a working demo of multithreaded program.
pthread.h
header file. This file contains the declarations related to all the thread related functions.main
function, I have created a array of type pthread_t
, which is nothing but an integer denoting the id of a thread.pthread_create
function. This function accepts four arguments. First is the memory address where the id of the thread being created has to be stored. Here we pass the address of the corresponding position in the array thread_num
for that thread. Second argument is some attributes for how to create the thread. We will pass NULL to accept the defaults. In third, we have to pass the function which will be executed by the thread that is being created. We pass thread_function
here. Fourth argument is data that we can pass to the function that the thread will execute. Here we pass the number of the thread as the argument.pthread_create
function will return 0 when success, and an non-zero value when there is some error in creating the thread. That’s why, I have wrapped that call in an if, to capture the error.thread_function
while the main thread, will continue running the loop, creating new threads.pthread_join
on each thread that was created. pthread_join
function is used to join another thread to the current thread. It is a blocking call, so until the other thread is not finished, the current thread will be blocked. It is always nice to clean up all the threads that you have created in a program this way.pthread_join
accepts two arguments. First is the thread id to which to wait for. Second is the memory address where to store the value returned the other thread. Here we pass NULL as we don’t have any value returned by the thread_function
.Run the above program like this,
thread.c
is the name of the file, and pthread
is the library which has the actual object files for all the functions. You would get something like the following as the output,
As it must be obvious from the output above, the different threads executes in different order and you can never predict their order. And sometimes this can lead to some very hard to find bugs in your program.
Consider this code,
When you run this program, there are chances that you will see different value of x each time you run it. On my machine, I got the following results on 5 execution of these program.
Suppose the current value of variable x
is 10. Then the second thread executes the instruction int i = x
and stores the value 10 in i
. But then this thread get context switched with the first thread, and the first thread executes the instruction x = x + 1
and increments the value of x
to 11. Now when the second thread comes back, it will increment i
to 11 and store these value in variable x
, replacing the old value of x
.
The reason this happenend, is non exclusive manipulation of the global variable x
by the two threads. Reading the value of x
and writing to it, is not an atomic operation. So, it may happen that after reading the value of x
by one thread, some other thread may modify this value of x
before the first thread gets the chance to save it’s value.
To solve this problem, we will have to use locks.
pthread_mutex_t
and initialize it in main function using pthread_mutex_init
function passing in the address of the mutex variable. The second argument is used for setting some extra attributes for the mutex variable, which we don’t need right now, so just pass in NULL.pthread_mutex_lock
and pthread_mutex_unlock
. Before we access the x
variable, we first acquire the lock on the mutex, and after we are done with x
, we unlock the mutex, so the other thread can now access the x
.pthread_mutex_destroy
function, passing in the address of the mutex variable.Properly dealing with mutexes and locks in your program can be a very challenging task. It requires some experience and good understanding of your program flow. Keep practicing, and it won’t be daunting task one day.
Before I finish this article, I just wanted to share one more example of using threads.
Below is the program which uses threads for doing merge sort. Feel free to go over the code. You should be able to understand the code as I have covered almost the functions in above examples.
Thank you for reading my article. Let me know if you liked my article or any other suggestions for me, in the comments section below. And please, feel free to share :)