Chapter 4. Multithreaded Applications with C/C++

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:

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.

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.

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:

Add the following code to this file:

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:

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:

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:

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:

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:

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:

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:

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.