In this chapter we will explore the Raspberry Pi 2 further via the C/C++ languages and a technology known as threads.
This chapter builds upon the previous material and will help you to understand how applications can run tasks concurrently. In addition to this, you will see how these concurrent operations can access a shared memory space and manipulate its value without overwriting each other's computations.
Topics also covered include the following:
To start with we will look at what threads are before writing several applications in both C and C++.
In order to understand threads, it helps to understand what a process is first.
Within Linux, if you run the command ps
you will see a list of processes running on the machine:
PID TTY TIME CMD 20215 pts/0 00:00:00 bash 20231 pts/0 00:00:00 ps
A process is a running instance of a program at a particular point in time. We can see in the example that both the ps
command we typed and the bash
shell is shown in the list.
Each process has a unique ID identifying it and has a number of other properties. These are:
At any point in time multiple processes could be running that need access to the register in the CPU, for example. It is the job of the short-term scheduler to decide which process is to be executed.
A thread can be thought of as a sub-division of a process and is the smallest unit a scheduler will work with to allocate resources.
Just as the operating system can have multiple programs running and a program can have multiple processes, a process can have multiple threads.
There are of course some differences between threads and processes. A process, for example, does not share its address space with that of another process; however, threads do.
Synchronization between processes is handled by the kernel or in some cases by technologies such as Message Passing Interface (MPI). Thread synchronization is handled within the process.
Within the process, each of these threads is executed asynchronously, and context switching between threads is far faster than between processes. This asynchronous approach allows us to divide our workload up between each thread within a program. As a result of this, a number of benefits can be found. The following example is used to illustrate this.
Take, for instance, a program that is required to process several inputs. These could be values returned from the Raspberry Pi's GPOI pins that take multiple readings, multiple socket connections, or some similar scenario.
If we did not have the option of using threads, the inputs would need to be processed one after another in a sequential fashion. This of course can slow down the program's ability to respond with certain outputs, if, for example, the processing of one input takes longer than expected or the operations needs to wait for another to complete.
In a system that requires real-time input and output this would be very inconvenient, for example, with a web server where multiple users may be trying to access a resource concurrently.
Several different types of thread exist to deal with these issues. Let's take a look at them.
There are three types of thread you may see discussed when writing applications. These are as follows:
We will quickly run through each of these types.
The first in our list is user level threads. These are created by user level libraries and not by the kernel, thus the kernel has no control over how they are processed.
In user level threads we follow a co-operative multitasking model where the thread is responsible for releasing the CPU when ready, rather than the scheduler assuming this role. This allows fast switching between different threads and thus lowers the overhead.
The next type of thread is kernel level threads. As you may have guessed, these are created and controlled by the kernel itself. There is a corresponding kernel level scheduled entity that maps to the user threads.
Unlike user level threads, kernel level threads implement a preemptive multitasking model. This results in the scheduler preempting a thread in execution and replacing it with a higher priority thread. This methodology therefore allows the scheduler to replace a blocked thread by an unblocked one, thus not holding up the process from executing.
Finally, we have hybrid threads, which act as a compromise between the kernel level and user level. In this instance, the threading library is responsible for the process of scheduling, which makes switching between threads very efficient as no system calls are required.
You can read more about these three models at https://en.wikipedia.org/wiki/Thread_(computing)#Processes.2C_kernel_threads.2C_user_threads.2C_and_fibers.
For a slide-based comparison of the pros and cons of each thread type, check out http://faculty.cs.tamu.edu/bettati/Courses/410/2014A/Slides/threads.pdf.
The library we will use in our C programs (pthreads
) is an interface that generates kernel level threads.
Now let's look at the standard model that this and many thread libraries are built to support: POSIX.
In Linux, we use POSIX thread libraries. You will see these referenced in C and C++ code with the pthread
header.
POSIX is a thread execution model that exists independently of the C language. Our pthreads library provides a C interface for interactions with this model.
When we import pthreads we thus have a wide variety of functions available to us. This includes the following:
The documentation for the standard can be acquired via the IEEE standards association website at http://standards.ieee.org/findstds/standard/1003.1-2008.html.
Let's look a little further into the steps involved in creating, managing, and terminating threads. We will discuss some of the areas in the bullet points in more detail.
When we want to implement a thread in our program there are several steps to be considered:
We will now look at each of these items and discuss what is involved.
This will give you a high-level overview of the process so you can understand what is happening in the later programs in this chapter.
Links to relevant documentation will also be provided for those interested in reading further.
The creation step is, as you have probably guessed, the process of generating a new thread. The termination step is the process of killing the thread.
When a thread is generated in Linux it has its own set of register values, a program counter, and its own call stack. It is also allocated memory where the stack is stored. These are used throughout the lifetime of the thread.
A detailed description of the creation process can be found at http://man7.org/linux/man-pages/man3/pthread_create.3.html.
Killing a thread involves the inverse of the creation process. In its simplest form the stack is removed and the memory is freed up.
A more detailed description including what happens with regards to mutexes and conditional variables can be found at http://man7.org/linux/man-pages/man3/pthread_exit.3.html.
In the C and C++ programs that follow you will see two functions in action that handle the creation and termination phases.
Once we have generated the threads we need to know how to coordinate, we will look at synchronization.
Thread synchronization is an important topic. When a program generates multiple threads we often need to organize them to avoid a number of pitfalls.
There are three key areas we are interested in:
A mutex is used to prevent multiple threads operating upon the same memory area at the same time. Without implementing mutexes we can encounter problems such as race conditions, inconsistent results, and data loss.
Therefore, a mutex can be implemented to take an asynchronous process and serialize it so that operations take place in a certain order. Thus we can guarantee the program will run consistently each time.
Further information on mutexes can be found at http://linux.die.net/man/3/pthread_mutex_init.
Following this are joins. These can be used to cause the program to pause until all the threads have finished executing. For example, say we wished to spawn four threads and have each of them estimate pi. We could then wait until each thread has finished executing, collect the results, and then run an average with the combined results.
For those interested you can read more about joins at http://linux.die.net/man/3/pthread_join.
Our final category is conditional variables. These allow us to perform operations such as halting a program, thus allowing the process to be used for another action until some state is true
. Conditional variables are used in conjunction with mutexes.
For more information on conditional variables, visit http://linux.die.net/man/3/pthread_cond_init.
When multiple threads come into play, a method is needed to say when to execute each one. This is achieved via scheduling. We will let Raspbian handle this, but will briefly explore the concept next.
Raspbian by default will continuously select a single unblocked thread for execution. This default behavior is optimal for the programs we use to demonstrate threading concepts.
Thread scheduling, however, allows a programmer to set the priority of threads and choose which algorithm/policy is used to control thread priority. This in essence allows a program to override the default behavior of the operating system.
The POSIX standard implementation via C allows us to control some details regarding how threads are scheduled. They can be implemented as follows:
Implementation of an override for scheduling is out of the scope of the programs in this book. However, for those of you who are interested, you can read more in Chapter 4, Managing Pthreads, in an excellent book called Pthreads, O ' Reilly, authored by Bradford Nichols, Dick Buttlar, and Jacqueline Proulx Farrell.
We shall now look at our first example in the C programming language.
In this program we will explore how to generate a number of threads that output text to the screen.
Here we have an example C program that implements the thread creation process.
Start by creating a new file under your c_programs
folder called second_c_prog.c
:
vim /home/pi/c_programs/second_c_prog.c
Add the following code to this file:
#include<stdio.h> #include<pthread.h> pthread_t thread_id[4]; void* thread_processor(void *arg) { pthread_t t_id = pthread_self(); printf("\n Thread %d processing\n", t_id); return NULL; } int main(void) { int i = 0; int error; while(i < 4) { error = pthread_create(&(thread_id[i]), NULL, &thread_processor, NULL); if (error != 0) { printf("\nthere was a problem creating thread: %s", strerror(error)); } else { printf("\n Thread number %d created.\n", i); } i++; } sleep(10); return 0; }
Let's now step through what we have here and discuss what it does.
At the top of the file we see we have included two header files:
#include<stdio.h> #include<pthread.h>
The first of these, stdio.h
, you are already familiar with from our first C program.
The second, pthread.h
, is new. This header stands for POSIX threads and contains the function references we need in order to implement threads in our application.
The next line of code we are interested in is this:
pthread_t thread_id[4];
Here we are defining an array of length 4
. This array by the name of thread_id
will contain 4
threads.
Next we come to our first custom function, thread_processor
:
void* thread_processor(void *arg) { pthread_t t_id = pthread_self(); printf("\n Thread %d processing\n", t_id); return NULL; }
The method is responsible for outputting the ID of the thread each time it is called. This is achieved through the pthread_self()
function. When this is invoked it returns the thread's ID to our t_id
variable.
Using the printf()
method we can output this to the screen.
Finally, we exit the function by returning NULL
.
Following this we define our main()
function. This is the main entry point into our application. Let's now take a look at some of the lines of code contained in it.
The first two lines are variable definitions:
int i = 0; int error;
The integer i
variable is used as a counter. Each time we create a new thread, at the bottom of the main
function we increment this variable:
i++;
Following this is another integer variable called error
. When we create a new thread it should return a code of 0
to show that no error was thrown. However, if for some reason the thread could not be created, the relevant error code is returned and stored in our variable.
We can therefore check the error variable after an attempt to spawn a thread to see if it was successful.
You may see some parallels here when we created our Assembly language program and returned a number to register 0
. This number could then be viewed via the echo
command. If our Assembly language program threw an error, this could have been stored in register 0
and when running echo $?
we could see the exit code that is the error code of the application.
Following this, we include a while
loop:
while(i < 4)
This ties into the code that increments the i
variable. While our variable is less than 4
(starting at 0
) we run the code in the while loop block. Once again, you will be familiar with this idea of a while
loop from our Assembly language programs.
Now we get to a very interesting piece of code, the thread creation:
error = pthread_create(&(thread_id[i]), NULL, &thread_processor, NULL);
As we just mentioned, the output of this is stored in error
, but let's look at the pthread_create
function. This function creates a new thread inside of the process, and it takes a number of parameters. The first we have passed in is a reference to the thread_id
array we specified at the top of the program. Following this, the second parameter is NULL
.
This second parameter is where we could pass in attributes to the thread creation process. If we leave it NULL
, as we have, the default attributes will be used.
You can read more about the POSIX thread attributes here http://man7.org/linux/man-pages/man3/pthread_attr_init.3.html.
Following this, we are passing in a reference to the thread_processor
function. This is the code we looked at earlier that displays the ID of the newly spawned thread.
Finally, once again we pass in NULL
. This final parameter is a void*
pointer that can point to any user data to be used as input to start_routine
when the thread starts. Since we have passed in NULL
, then a NULL
pointer is used as an input parameter.
This concludes the call to generate a thread. Following this, we use an if else
statement to display a message to the screen. If we successfully generated a thread we communicate this; otherwise, we output the error code.
Now we have walked through our program, it's time to try it out.
We can compile this with gcc
as follows:
gcc -pthread -o second_c_prog second_c_prog.c
In this command we have linked the pthread
library with our own code so we can implement it into the executable file.
Once compiled, you can run the application from the command line:
./second_c_prog
Something like the following should be output to the screen:
Thread 1994273904 processing Thread number 0 created. Thread number 1 created. Thread 1985885296 processing Thread number 2 created. Thread 1977496688 processing Thread number 3 created. Thread 1969108080 processing
To exit from the program you can press Ctrl+ C.
Now that we have completed our second C program, let's take a look at how we might write this application in C++.