Concurrency in C++: Threads, Challenges, and Examples

Why C++ Remains a Powerful Programming Language

C++ is one of the fastest language available to programmers. Even if the language is old there are constant updates of new features that makes the language modern and interesting to use. The ambition with my blog it to provide insights in various areas of C++ written in a simple language with a personal touch. I hope my background as a lecturer and from the finance industry can make the reading of my text attractive. I start with a discussion about concurrency.

Introduction of Concurrency in C++11

In the C++11 update concurrency was introduced and that have significantly improved performance. That is, of course, the purpose of most updates. To use the increased performance concurrency provides it is essential to have a modern hardware with multicore systems and a number of hardware threads. If you have that then to take advantage of the great performance of your computer it is a good idea to designed your code to run multiple tasks concurrently. An efficiency problem is that there is an overhead cost of launching a thread, therefor, if the task to perform is completed quickly a concurrent code might not provide the advantage the coder expects.

The Challenges of Learning Concurrent Programming in C++

It was many years ago concurrency caught my interest. A sharp-eyed reader has probably already seen the picture on the front of this web page. To the right there is an example of a concurrent code. A downside of a concurrent code is the work of learning it. The learning curve is steep and there are some traps that can cause problems. For most C++ programmers a rumour of steep learning curve sounds challenging and gives a strong desire to climb this learning curve to overcome an obstacle.

Common Concurrency Pitfalls: Deadlocks and Race Conditions

What are the main traps that can go wrong in a concurrent code? The first two thing you think about are deadlock and race conditions. You don’t meet that anywhere else. Race conditions can occur when two or more threads try to access the same resource. An example could be when there is a limited amount deposited at a bank-account and two resources tries to access that amount at the same time. They will block each other and neither of them will access the amount. Deadlock occurs when two threads are waiting for the other thread to do something or for a specific resource that is needed by another thread as well. 

Getting Started with Threads in C++

All those things will be discussed later but we start to see what a thread looks like. Assume you have a function in a class and you want to perform a task. In this case multiply two numbers. Very easy to perform and good for illustration. Start by create an object and initialise values in the constructor.


Example 1: Running a Class Function in a Thread

#include <iostream>
#include <thread>
using namespace std;

class Multiply
{
public:
	Multiply(int nr_1, int nr_2) :in_nr_1(nr_1), in_nr_2(nr_2) {};
	void numbers() const {
	int answer = in_nr_1 * in_nr_2;
	cout << "The answer is: " << answer << "\n";
	}

private:
	int in_nr_1, in_nr_2;
};

int main()
{
Multiply c(5,8);
thread t1{&Multiply::numbers,&c};	//Pass address
thread t2{&Multiply::numbers,ref(c)};	//Pass reference
thread t3{&Multiply::numbers,c};	//Pass object
t1.join();
t2.join();
t3.join();
}

The Importance of join() in Thread Management

The function in the class is declared void because a thread can’t return a value but the result can be written to the screen using cout. Start by creating an object and initialise values in the constructor. Then you launch the thread. You pass the address to the function and then you can pass the address or a reference to the object. In the third thread the object itself is passed and that can also be done. Now the original object does not matter and can be destroyed. Don’t forget the function join(). That is necessary to make sure that the thread has completed its execution before the program exits. The function join() is a blocking call. It cleans up any storage associated with the thread.

Example 2: Using Function Objects with Threads

Instead of creating an object you can use a function pointer and let it be executed in the thread. A function object is a class that implements a function object and you pass information to the thread by passing arguments to the function. If you write an operator() for your class you can use objects of that class as if they were function pointers. Let’s see how it works.

#include <iostream>
#include <thread>
#include <vector>
#include <numeric>
#include <iterator>
using namespace std;

class Function_Obj
{
public:
	Function_Obj(int First, int Nr): firstNr{First},Numbers{Nr}{}
	
	void operator()() {
		length = Numbers - firstNr;
		vector<int> Vec(length);
		size_t s = Vec.size();
		iota(begin(Vec), end(Vec), firstNr);
		ostream_iterator<int> out(cout, " , ");
		copy(cbegin(Vec), cend(Vec), out);
	}
private:
	int firstNr, Numbers, length { 0 };
};

int main()
{
	thread t1{ Function_Obj{5,20} };
	t1.join();
}

Understanding the Role of iota and copy in Threaded Code

Here we have a class called Function_Obj. The class has a constructor that takes two numbers and we will get a vector with a sequence of number with a start value and a value to calculate the size of the vector. The vector is filled with values using the numerical algorithm iota. Iota fills a sequence successively incrementing values stating with a given value. In this case firstNr. The numbers are printed using a ostream_iterator and the predefined algorithm copy(); The thread is initialised using the uniform syntax. An instance of the class is created with its constructor arguments to give it to the constructor argument between curly braces.

Next Steps: Returning Values from Threads in C++

In the examples above we have given two examples of how to use threads. To use the values from the threads we need to return them. We work on that next time.

Previous
Previous

How to use values from a thread