Concurrency in C++ is a powerful feature that enables developers to create applications capable of performing multiple tasks simultaneously. In this article we'll try to understand fundamentals of concurrency using inbuilt library of modern C++ i.e. C++ 11 and beyond.
Concurrency in software refers to the ability to process multiple tsks or threads at the same time. It is used to enhance the performance and response time of an program.
1. Introduction
1.1 What Is Concurrency?
At the simplest and most basic level, concurrency is about two or more separate activities
happening at the same time. We encounter concurrency as a natural part of life;
we can walk and talk at the same time or perform different actions with each hand,
and we each go about our lives
1.2 Concurrency vs. Parallelism
Concurrency and parallelism have largely overlapping meanings with respect to multithread code. The primary difference is matter of nuance, focus and intent. Both terms refers to running multiple task simultaneously, using available hardware. Parallelism focuses on performance i.e. take advantage of available hardware to increase the performance of bulk data processing. Concurrency on the other had focuses on separation of concern or responsiveness.
1.2 Using Concurrency For Performance
In last few decades chip manufactures have started releasing multi processor system. In recent time chip manufacturers have also started adding multiple cores in same processor to increase the processing power. If software is to take advantage of increased computing power, engineers need to design software to support running multiple task concurrently.
There are two ways to use concurrency for performance:
Task Parallelism:
This is the most obvious approach, i.e. divide single task into parts and run each in parallel to reduce the runtime.
Data Parallelism:
In this approach, each thread performs same operation on different part of data.
2. Multithreading Basics
One of the most significant new features in the C++11 Standard was the support of
multithreaded programs. With the introduction of inbuilt support for multithreaded C++ program, it made possible to write C++ program without relying on platform specific extension, making code more portable with guaranteed behavior.
2.1 Hello Concurrent World
Lets start concurrency journey with classic example of "Hello World". A simple Hello World program runs in a single thread is shown in below example.
// Example 1
#include <iostream>
int main(){
std::cout<<"Hello World\n";
return 0;
}
Output
Hello World
Above Example 1 program simply prints "Hello World" string on standard out console. Lets rewrite the same example using multi threaded library.
// Example 2
#include <iostream>
#include <thread>
void hello(){
std::cout<<"Hello Concurrent World\n";
}
int main(){
std::thread t(hello);
t.join();
return 0;
}
Output
Hello Concurrent World
Referring to Example 2, we can see inclusion of thread library via #include<thread> which is part of Standard C++ Library used to manage threads. Second major difference wrt Example 1 is to print message on console, we've used separate function hello(). In order to launch hello() as separate thread, we've instantiate thread library object t using std::thread t(hello) statement where std::thread constructor takes function or method which needs to be executed as separate thread as an argument along with other optional argument which needs to be passed on to the function while running them into separate function. After this statement we've called t.join() which wait for the hello() function to end its execution and join back to the main thread.
If we do not want our main thread to wait for hello() thread, we can modify the above example by replacing call to join() with detach(). Call to detach() on thread object leaves the thread running in background with no direct communication with it. In detach mode ownership is passed on to the C++ Runtime Library, which ensures that the resources associated with the thread are reclaimed once thread exits.
// Example 3
#include <iostream>
#include <thread>
void hello(){
std::cout<<"Hello Concurrent World\n";
}
int main(){
std::thread t(hello);
t.detach();
return 0;
}
We will not dwell into when to use join or detach but above simple example shows us the inbuilt library is versatile enough and give flexibility to engineer to manage the threads.
2.2 Passing Argument To Thread Function
Below example 4 shows how to pass argument to callable object or function using std::thread constructor.
// Example 4
#include <iostream>
#include <thread>
void task(int x){
std::cout<< x << std::endl;
}
int main(){
std::thread t(task, 100);
t.detach();
return 0;
}
Output
4
It is important to note, by default the arguments are copied into internal storage, where they can be accessed by the newly created thread of execution and then passed to the callable object or function as rvalue as if they were temporaries. We will explore these concepts in future article and how the impact program, especially if we want to modify the container or buffer via thread.
Lets try to understand this with the help of simple example.
void f(int i,std::string const& s);
std::thread t(f,3,"hello");
Above code snippet creates new thread t which executes f( 3, "hello"). f() takes second parameter as std::string, string literal is passed as char const* and converted into std::string only in the context of the new thread.
void f(int i,std::string const& s);
void oops(int some_param) {
char buffer[1024];
sprintf(buffer, "%i",some_param);
std::thread t(f,3,buffer);
t.detach();
}
In above example, second parameter passed to f() is pointer to local variable buffer while running as new thread. There is significant chance that oops() function will exit before the buffer has been converted to std::string on new thread, leading to undefined behavior. Below is the modified version of earlier code which avoid undefined behavior caused by potential dangling pointer.
void f(int i,std::string const& s);
void oops(int some_param) {
char buffer[1024];
sprintf(buffer, "%i",some_param);
std::thread t(f,3,std::string(buffer)); //Using std::string avoid dangling pointer
t.detach();
}
2.3 Choosing The Number Of Threads At Runtime
C++ Standard Library supports reading capabilities of underlying hardware/processor using std::thread::hardare_concurrency(). This function returns an indication of the number of threads that can truly run concurrently for a given execution of a program. For a multicore system it might be number of CUP core. Programmers can leverage this information to write applications which automatically adjust no. of thread based on underlying hardware.
2.4 Identifying Threads
C++ Â Standard Library have thread identifier of type std::thread::id and below example shows how to read thread id using two approach. First approach is to use get_id() member function on the thread object. Second approach is to use std::this_thread::get_id() to get current thread's id.
// Example 5
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
void task(int x){
cout<<"Thread no: " << x << "\t TID: "
<< std::this_thread::get_id()
<< endl; // alternative to get thread id
}
int main(){
vector<thread> threads(10);
for(int i= 0; i < threads.size(); ++i) {
threads[i] = thread(task, i);
cout<<"i: " << i
<< " id: " << threads[i].get_id()
<< endl;
}
for(int i = 0; i<threads.size(); ++i) {
threads[i].join();
}
return 0;
}
Output
Thread no: 0 TID: 136601032181312
i: 0 id: 136601032181312
i: 1 id: 136601023788608
i: 2 id: 136601015395904
i: 3 id: 136601007003200
i: 4 id: 136600998610496
i: 5 id: 136600990217792
i: 6 id: 136600981825088
i: 7 id: 136600897975872
i: 8 id: 136600889583168
i: 9 id: 136600881190464
Thread no: 4 TID: 136600998610496
Thread no: 7 TID: 136600897975872
Thread no: 9 TID: 136600881190464
Thread no: 6 TID: 136600981825088
Thread no: 5 TID: 136600990217792
Thread no: 3 TID: 136601007003200
Thread no: 2 TID: 136601015395904
Thread no: 1 TID: 136601023788608
Thread no: 8 TID: 136600889583168
Above example 5 creates multiple threads which are handled via vector<thread> object. Using both methods we've printed thread ids of various threads and the thread ids do not represent anything specific apart from the fact if two thread ids are equal then they represent the same thread. Thread id can be used to perform specialized action for certain type of thread.
Summary
In this article we've covered how to create thread and mange these threads via C++ Standard Library. In our next articles we will cover what are the challenges when writing multi-threaded program and how to over come those.
Reference:
A Tour of C++ - Bjarne Stroustrup
C++ Concurrency In Action - Anthony Williams
Comments