{this.article.is.in.construction}
C++ is a very complicated langage, concurrency and threading are a extremely complex subjectr of the matter, if that was not even enough, operating system bring a layer on top of all that. While being a powerful tool we have since C++ 11, std::thread comes also with a lot to take in the matter, such as now the threads are actually created from the operating system perspective, what is actually a thread versus a process etc… It feels like the whole subject of that matter could be easily resumed as few lines of codes, yet when you try to properly allign those ideas and concepts, you realize that multithreading is not only a way to approach problem, but to also dissect stuff that should not have been because, threading does not make everything fast out of the box, and so on.
Process vs thread :
One of the first thing I want to point out is the difference between a process and a thread… While this area of computing could have its own tresearus, I would like to define a bit of our langage thorugut this article. First of, we need to break into some layering the matter of the facts, wich are threads(in C++).
------------ ----------
CPU Hardware
------------ ----------
↓ ↓
------------
Kernel/Drivers
------------
↓
↓
↓
------------
Memory Management
------------
↓
↓
↓
-------------------------------
OS
-------------------------------
↓
↓ ↓
↓ ↓
↓ ↓
--------------- ---------------
Process1 Process2
--------------- ---------------
↓ ↓
------------ ------------
Virtual Mem 1 Virtual Mem 2
------------ ------------
↓ ↓
* *
* *
* *
*-> myprogram *-> otherprogram
In this diagram, I try to achieve a very simple abstraction of every layering on how a simple c++ program would run on Linux, Windows or every operating system. Please note that this is way more complicated as the operating system perform a lot of abstraction, but for this first part, I wanted to express a bit more of the langage we will use.
In many articles online, you will find a lot of writters refering to process as the result of abstraction from the OS, making sure that everything from virtual memory assignment, paging and so on is correctly abstracted from the OS. Once again, we need to remember then when we run a simple C++ program from, per say, the terminal, we simply ask the operating system to launch a new process, this process, as complicated it can be, have its own “virtual-ish” environnement on wich to run.
While being a bit counter-intuitive that we actually want to get closer to hardware using std::thread by understanding how the os virtualize it will pay a bit more later. Operating system are insanely complex area of computer science wich I do not want to cover in this article, because it would be so long and quite away of the subject. But I do feel that by understanting the basic abstraction from the Operating system to the program you run in C++ using std::thread, you will enjoy how beautiful and powerfull is the standard template library.
If you want to know a bit more about how every virtual memory is built and allow the process to use virtual memory without directly mapping it to hardware memory, I would suggest chapter 3 of the book OS, 3 easy pieces.
So lets assume that in our perfect world of analysis we have thread running on a perfectly abstracted process that we can now carve into our needs using the C++ langage to create a program… This is where, how journey really starts.
Memory of the process, overview:
While we abstracted a bit leveraging the work into the OS, we still need, as programmer to be able to have a common understanding on what is a process and what composes it. It would be simple to simply assume that the OS will handle all the work, but it is a little bit more complex, and this is why C++ thread are so powerful. We have, into our hands, full control about how does the program runs everything together and not together, waiting, hidhling, waiting for something to come for something to execute, waiting to wait to sleep and so on.
For now on, lets zoom a bit more on the memory virtual memory layout of a process:
+----------------------------------------------+
| VIRTUAL MEMORY |
+----------------------------------------------+
| process |
| * |
| * +--------------------+ |
| * | Static | |
| * | | |
| +--------------------+ |
| | | |
| | Heap | |
| | (dynamically grows | |
| | downward) | |
| | | |
| +--------------------+ |
| | | |
| | Code | |
| | (Text/Code) | |
| | | |
| +--------------------+ |
| |
| |
| +--------------------+ |
| | | |
| | Stack | |
| | (dynamically grows | |
| | upward) | |
| | | |
| +--------------------+ |
+----------------------------------------------+
^ Higher addresses ^
| |
| |
v Lower addresses v
lets review a bit on each of those memory spaces, but most notably the stack and the heap:
heap memory:
very large, quite slow, everything needs to be explicitly mentionned in the program. Everytime you add a thing to the heap, you need to track it, delete it, manually handle every operations of the memory in the C++ program.
In code:
// heapallocation.cpp
int* ptr_int = new int(12);
One note here you can probably new is that we use a pointer to allocate the value on the heap, and it is a very important point to mention: In C++, when we use the new keyword, we delegate the allocation of the memory to the process, therefore, the hardcoded value 12 will be assigned to the heap, but we track it using the memory location(address ) on the heap. The stack holds a pointer to the heap adress.
Therefore, we need to note that 12 is not on the stack, what we have now on the stack is a pointer to the address on the heap. and you are the one and only, as the programmer responsible to delete it when you do not need it anymore.
stack memory:
90% of the works goes there, as it uses the good old fashioned FIrst if first out.
A Simple thread wrap…
One of the first concept I like to express for thread -safety-ish is the fact that if you wrap a naked thread into a class, you have a bit more flexibility over how this thread behave in your whole program. The idea here is to simply wrap the thread into what is the concept that when a Class is reaching out of scope in C++, it is being destroyed.
// g++ -pthread threadwrapper.cpp -o threadwrapper
#include <iostream>
#include <thread>
//
class ThreadWrapper
{
public:
std::thread localThread;
ThreadWrapper(std::thread t)
{
localThread = std::move(t);
}
~ThreadWrapper()
{
if(localThread.joinable())
{
localThread.join();
}
}
};
void threadThat()
{
std::cout << "Shit in thread" << std::endl;
}
void CallThreadWork()
{
std::cout << "Init thread works..." << std::endl;
ThreadWrapper thread{std::thread(threadThat)};
std::cout << "All threads correctly called";
}
int main()
{
CallThreadWork();
return 0; // Return 0 to indicate success
}
While far from being perfect, by using this simple wrapper, we can run the program and have no runtime error(for the sake of the simplest threaded ever program). It gets a lot more complicated, and the flavour on how you build your wrapper and design your calling strategy will grow a lot deeper as we dive in a bit deeper into abstraction. But for now, we need to ensure that the thread is luanched and handled, by joining it inside the destructor.
But we have a problem:
It seems like the threaded work is not properly synched… Almost like the function that calls the threadwrapper exits and the thread still works.
$ ./threadwrapper
Init thread works...
All threads correctly calledShit in thread
Lets add a timer to fix that…(This is for demonstration only, as it is very bad design)
// g++ -pthread threadwrapper.cpp -o threadwrapper
#include <iostream>
#include <thread>
#include <chrono> // Added for sleep_for functionality
class ThreadWrapper {
public:
std::thread localThread;
ThreadWrapper(std::thread t) {
localThread = std::move(t);
}
~ThreadWrapper() {
if(localThread.joinable()) {
localThread.join();
}
}
};
void threadThat() {
std::cout << "Starting thread work..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "Shit in thread" << std::endl;
}
void CallThreadWork() {
std::cout << "Init thread works..." << std::endl;
ThreadWrapper thread{std::thread(threadThat)};
std::cout << "All threads correctly called" << std::endl;
}
int main() {
CallThreadWork();
return 0; // Return 0 to indicate success
}
./threadwrapper
Init thread works...
All threads correctly called
Starting thread work...
Sleeping for 2 seconds...
Shit in thread
While it sleeps, we can still see that the function print the statement All threads correctly called. But now, we have one thing sure, is that the thread is indeed running on its own,
The key point is that some output appears out of order because the main thread continues executing while the new thread runs in parallel. The ThreadWrapper
destructor ensures the program waits for the thread to finish before exiting.
What if I call detach on the thread?