Concurrency and Multithreading in C++: Advanced Level
\n\nModern applications demand maximum performance, and multithreading in C++ is one of the key tools for achieving it. However, basic mechanisms (std::thread, std::mutex) often lead to deadlocks, data races, and reduced efficiency. In this article, we will explore advanced techniques for working with concurrency in C++: from atomic operations to lock-free data structures and asynchronous tasks.
\n\n1. Atomic Operations and Memory Ordering
\n\nClassic mutexes are a heavy mechanism. For simple counters or flags, it is better to use std::atomic. It guarantees atomicity of read/write without locks. However, at an advanced level, it is important to understand memory ordering — the C++ memory model that defines the visibility of changes between threads.
Main memory orders:
\n- \n
memory_order_relaxed— minimal guarantees, only atomicity. \n memory_order_consume— rarely used, depends on data. \n memory_order_acquire— all subsequent reads/writes cannot be reordered before this operation. \n memory_order_release— all previous writes become visible to other threads after this operation. \n memory_order_acq_rel— combination of acquire and release. \n memory_order_seq_cst— the strictest, global sequential consistency (default). \n
Usage example:
\n#include <atomic>\n#include <thread>\n#include <cassert>\n\nstd::atomic<bool> ready{false};\nint data = 0;\n\nvoid producer() {\n data = 42; // write data\n ready.store(true, std::memory_order_release); // publish flag\n}\n\nvoid consumer() {\n while (!ready.load(std::memory_order_acquire)) // wait for flag\n ;\n assert(data == 42); // guaranteed to see data == 42\n}\n\nint main() {\n std::thread t1(producer);\n std::thread t2(consumer);\n t1.join();\n t2.join();\n return 0;\n}\n\nHere release and acquire guarantee that the write to data happens before setting the flag, and the read happens after. This is faster than a mutex.
2. Lock-Free Data Structures
\n\nLock-free algorithms avoid locks by using atomic operations and CAS (Compare-And-Swap). A classic example is a lock-free stack based on a singly linked list.
\n\nImplementation of a lock-free stack:
\n#include <atomic>\n#include <memory>\n\ntemplate<typename T>\nclass LockFreeStack {\nprivate:\n struct Node {\n T data;\n Node* next;\n Node(const T& val) : data(val), next(nullptr) {}\n };\n std::atomic<Node*> head{nullptr};\n\npublic:\n void push(const T& val) {\n Node* new_node = new Node(val);\n new_node->next = head.load(std::memory_order_relaxed);\n while (!head.compare_exchange_weak(\n new_node->next, new_node,\n std::memory_order_release,\n std::memory_order_relaxed)) {\n // loop until CAS succeeds\n }\n }\n\n bool pop(T& result) {\n Node* old_head = head.load(std::memory_order_relaxed);\n while (old_head != nullptr &&\n !head.compare_exchange_weak(\n old_head, old_head->next,\n std::memory_order_acquire,\n std::memory_order_relaxed)) {\n // wait\n }\n if (old_head == nullptr) return false;\n result = old_head->data;\n // Note: real code requires garbage collection or hazard pointers\n delete old_head;\n return true;\n }\n};\n\nImportant notes:
\n- \n