lezione 07

concorrenza e thread

thread, sincronizzazione e pattern per sistemi realtime affidabili.

livello: avanzato durata: 45 min output: task parallelizzati badge: concurrency

perché concorrenza

in c++ la concorrenza è fondamentale per sfruttare cpu multi-core, ridurre la latenza e isolare task costosi. la chiave è definire confini chiari: dati immutabili, ownership esplicita e sincronizzazione minima.

obiettivo: parallelizzare lavoro senza introdurre data race o deadlock.

thread e lifecycle

crea thread solo quando il task è davvero parallelizzabile, poi gestisci sempre join o detach.

#include <thread>
#include <iostream>

void render_pass(int id) {
    std::cout << "pass " << id << std::endl;
}

int main() {
    std::thread worker(render_pass, 1);
    worker.join();
    return 0;
}

usa std::jthread (c++20) per auto-join e stop token.

mutex e lock

proteggi le sezioni critiche con lock a scope. evita lock lunghi o annidati.

#include <mutex>
#include <vector>

std::mutex mtx;
std::vector<int> queue;

void push(int v) {
    std::lock_guard<std::mutex> lock(mtx);
    queue.push_back(v);
}
strumento quando usarlo
std::mutex proteggere sezioni critiche brevi
std::shared_mutex molti lettori, pochi scrittori
std::condition_variable coordinare thread con eventi

atomic e memory order

per contatori e flag usa std::atomic per evitare lock.

#include <atomic>

std::atomic<int> frames{0};

void tick() {
    frames.fetch_add(1, std::memory_order_relaxed);
}

usa memory_order_relaxed solo quando l'ordine non è critico. altrimenti acquire/release.

pattern moderni

  • task queue con thread pool per lavori piccoli e numerosi.
  • pipeline a stadi: parsing → trasformazione → output.
  • immutabilità: dati read-only condivisi senza lock.
auto future = std::async(std::launch::async, [] {
    return heavy_compute();
});

int result = future.get();

errori comuni

  • data race su variabili globali non protette.
  • deadlock da lock annidati in ordine diverso.
  • creazione eccessiva di thread per task microscopici.

tip: misura sempre l'overhead. a volte un algoritmo seriale è più veloce.

checklist finale

  • definisci ownership dei dati prima di parallelizzare.
  • usa lock a scope e riduci la sezione critica.
  • preferisci atomics per contatori e flag.
  • profiling prima e dopo ogni ottimizzazione.

panoramica

In questo capitolo su concorrenza e thread, thread, sincronizzazione e pattern per sistemi realtime affidabili. L'obiettivo è trasformare i concetti in micro-pattern riutilizzabili con esempi piccoli e verificabili.

Lavora in sequenza: leggi, prova, modifica gli snippet e annota i trade-off principali (performance, leggibilità, manutenzione).

badge: concurrency

obiettivi

  • capire e applicare perché concorrenza in uno scenario reale
  • capire e applicare thread e lifecycle in uno scenario reale
  • capire e applicare mutex e lock in uno scenario reale
  • capire e applicare atomic e memory order in uno scenario reale

scheda rapida

#include <iostream>
#include <vector>

int main() {
  std::vector<int> dati{1, 2, 3};
  for (auto &v : dati) {
    v *= 2;
  }
  for (const auto &v : dati) {
    std::cout << v << " ";
  }
  std::cout << "\n";
  return 0;
}

Adatta questo scheletro agli esempi della lezione e sostituisci i dati con il tuo dominio.

tips

  • compila con warning elevati
  • preferisci RAII
  • usa const ovunque possibile
  • proteggi dati condivisi
  • usa std::lock_guard
  • evita data race

tip: La concorrenza senza design = bug non deterministici.

mini progetto

Contatore thread-safe di base.

  • crea mutex
  • incrementa in thread
  • join dei thread
  • verifica output

output atteso: uno script o query ripetibile con risultati verificabili.

start a brief