Programare Concurentă și Paralelism în C++: Modelul de Memorie, Atomics și jthread în C++

Ghid aprofundat de multithreading și programare concurentă în C++: gestionarea sigură a ciclului de viață cu std::jthread și std::stop_token, înțelegerea modelului de memorie (Sequential Consistency vs Acquire-Release vs Relaxed) și algoritmi Lock-Free.

Sintaxă
std::jthread t([](std::stop_token st) { while(!st.stop_requested()) ... });

Scrierea codului concurent și multithreading de înaltă performanță în C++ necesită o înțelegere riguroasă atât a abstracțiunilor de nivel înalt (thread-uri, mutex-uri, lock-uri), cât și a comportamentului fizic al hardware-ului (reordonarea instrucțiunilor de către compilator și procesor, bariere de memorie și ierarhia de cache-uri CPU). Începând cu standardul C++20, lucrul cu firele de execuție a devenit mult mai sigur prin introducerea clasei std::jthread și a semnalelor cooperative de oprire.

1. std::jthread: Înlocuitorul Modern și Sigur pentru std::thread

Clasa clasică std::thread (C++11) avea un defect major de design: dacă uitați să apelați .join() înainte ca obiectul să iasă din scope, destructorul invoca imediat std::terminate(), prăbușind întreg programul! std::jthread rezolvă acest neajuns executând automat join() în destructor și oferă suport integrat pentru anulare cooperativă prin std::stop_token:

Oprirea cooperativă curată a unui fir de execuție cu std::jthread
#include <thread>
#include <chrono>
#include <print>

void workerProcesare(std::stop_token stopToken, int workerId) {
    std::println("Worker-ul {} a pornit.", workerId);

    // Firul rulează continuu până când i se cere oprirea:
    while (!stopToken.stop_requested()) {
        std::println("Worker-ul {} proceseaza date...", workerId);
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }

    std::println("Worker-ul {} a interceptat cererea de oprire si s-a incheiat curat.", workerId);
}

int main() {
    {
        // Lansăm un jthread (trece automat stop_token ca prim parametru):
        std::jthread t(workerProcesare, 1);

        std::this_thread::sleep_for(std::chrono::milliseconds(600));
        std::println("Main cere oprirea worker-ului...");
        t.request_stop(); // Trimite semnalul de oprire cooperativ
        // LA IEȘIREA DIN SCOPE: t.~jthread() apelează automat t.join()!
    }

    std::println("Programul principal s-a incheiat in siguranta.");
    return 0;
}

2. Modelul de Memorie C++ (Memory Model) și std::memory_order

Când mai multe fire de execuție citesc și scriu date partajate fără un mutex, se utilizează tipurile atomice std::atomic<T>. Pentru performanță maximă, putem controla precis gradul de sincronizare a memoriei prin parametrul std::memory_order:

Ordine de Memorie Comportament Hardware Cost CPU Caz Tipic de Utilizare
memory_order_relaxed Garantează doar atomicitatea operațiunii (fără data race). Nu impune nicio barieră de memorie sau sincronizare între thread-uri. Minim (instrucțiune atomică pură) Contoare de vizitatori, metrici statistice, indecși de distribuire.
memory_order_acquire / release Modelul producător-consumator: release garantează că toate scrierile anterioare sunt vizibile pentru thread-ul care execută acquire. Moderat Cozi de mesaje concurente (Lock-Free Queues), semnalizări flag.
memory_order_seq_cst (implicit) Secvențialitate strictă globală: toate thread-urile văd exact aceeași ordine a tuturor operațiilor din întreg programul. Maxim (bariere complete de memorie) Cod concurent complex unde siguranța este primordială.

3. Fenomenul de False Sharing și Evitarea Sa

Dacă două variabile atomice utilizate intens de două thread-uri diferite se află pe aceeași linie de cache (Cache Line – 64 octeți), nucleele CPU își vor invalida reciproc cache-ul L1 la fiecare scriere, degradând viteza de până la 50 de ori. Soluția este alinierea explicită a fiecărei variabile la dimensiunea liniei de cache hardware:

#include <new>

struct alignas(std::hardware_destructive_interference_size) ContorIzolat {
    std::atomic<uint64_t> valoare{0};
};