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.