Le soluzioni al problema della sezione critica si dividono tra quelle di tipo hardware e quelle di tipo software.

Soluzioni hardware

Definizione: soluzione hardware

Una soluzione hardware al problema della sezione critica è una soluzione che garantisce il rispetto del requisito della mutua esclusione attraverso il supporto del sistema operativo o tramite istruzioni hardware specifiche.

Barriere di memoria

I processori e/o i compilatori possono riordinare le istruzioni o ritardare l’aggiornamento della memoria principale per ottimizzare la velocità dei programmi.

Per risolvere questo problema, bisogna agire a livello hardware: per questo sono state introdotte le barriere di memoria.

Definizione: barriera di memoria

Una barriera di memoria (o recinzione di memoria, in inglese memory barrier o memory fence) è un’istruzione che impedisce al processore di riordinare le letture e scritture di memoria al di là della barriera, garantendo che tutte le operazioni di memoria prima della barriera siano completate e visibili alle altre unità di elaborazione prima di eseguire le operazioni di memoria dopo la barriera.

Istruzioni hardware atomiche

Molte delle moderne architetture offrono particolari istruzioni che permettono di effettuare operazioni di lettura e scrittura contemporaneamente in modo atomico, cioè come un’unità non interrompibile, permettendo così la risoluzione del problema della sezione critica in modo relativamente semplice.

Test-and-set (TAS)

Definizione: test-and-set (TAS)

Il test-and-set (TAS) è una soluzione hardware al problema della sezione critica implementabile attraverso l’utilizzo di un’operazione atomica, cioè indivisibile e non interrompibile da interruzioni.

Si implementa attraverso una variabile booleana condivisa, detta , che indica se la risorsa contesa è libera o occupata, e una funzione che prende come argomento il , lo imposta a (per indicare che la risorsa è occupata) e restituisce il suo valore precedente:

bool test_and_set(bool *lock) {
	bool value = *lock;
	*lock = true;
	return value;
}

Prima di entrare nella sezione critica, il programma esegue un ciclo che esegue la sul e, se quest’ultimo indica che la risorsa era precedentemente libera (), allora può entrare nella sezione critica, altrimenti aspetta finché non si libera attraverso il meccanismo del busy waiting:

while (test_and_set(&lock)); // Busy waiting
// Esecuzione della sezione critica
lock = false;
// Esecuzione della sezione non critica

Compare-and-swap (CAS)

Definizione: compare-and-swap (CAS)

Il compare-and-swap (CAS) è una soluzione hardware al problema della sezione critica implementabile attraverso l’utilizzo di un’operazione atomica, cioè indivisibile e non interrompibile da interruzioni.

Si implementa attraverso tre variabili, di tipo puntatore a intero ed e di tipo intero, e una funzione che le prende come argomenti. La funzione si salva il valore di in una variabile temporanea di tipo intero e, se questo corrisponde a quello di , viene sostituito con ; infine restituisce il valore originale:

int compare_and_swap(int *value, int expected, int new_value) {
	int temp = *value;
	if (*value == expected) *value = new_value;
	return temp;
}

Prima di entrare nella sezione critica, il programma esegue un ciclo che esegue la  sul  e, se quest’ultimo indica che la risorsa era precedentemente libera (), allora può entrare nella sezione critica, altrimenti aspetta finché non si libera attraverso il meccanismo del busy waiting:

while (compare_and_swap(&lock, 0, 1) != 0);
// Esecuzione della sezione critica
lock = 0;
// Esecuzione della sezione non critica

Una versione di che rispetta anche il requisito dell’attesa limitata è la seguente:

waiting[i] = true;
key = true;
while (waiting[i] && key == true) key = compare_and_swap(&lock, false, true);
waiting[i] = false;
// Esecuzione della sezione critica
j = (i + 1) % n;
while ((j != i) && !waiting[j]) j = (j + 1) % n;
if (j == i) lock = false;
else waiting [j] = false;
// Esecuzione della sezione non critica

Variabili atomiche

In genere, il compare-and-swap non viene utilizzato direttamente per fornire la mutua esclusione, ma serve piuttosto da elemento base per la costruzione di altri strumenti che risolvono il problema della sezione critica.

Uno di questi strumenti è la variabile atomica.

Definizione: variabile atomica

Una variabile atomica è una variabile su cui sono possibili operazioni di lettura e scrittura solo in modo atomico, cioè senza la possibilità di poter essere interrotte.

Esempio di uso delle variabili atomiche

L’incremento o il decremento di un valore intero può produrre una Race condition: le variabili atomiche possono essere utilizzate per garantire la mutua esclusione in situazioni in cui potrebbe esserci una Race condition su una singola variabile durante il suo aggiornamento, come quando un contatore viene incrementato.

La maggior parte dei sistemi operativi che supportano le variabili atomiche fornisce speciali tipi di dati atomici e funzioni per l’accesso e la manipolazione di variabili di tali tipi, queste ultime spesso implementate usando il compare-and-swap. Per esempio, una funzione per incrementare in modo atomico un intero potrebbe essere la seguente:

void increment(atomic_int *v) {
	int temp;
	do {
		temp = *v;
	} while (temp != compare_and_swap(v, temp, temp+1));
}

Soluzioni software

Analizziamo ora le soluzioni software al problema della sezione critica.

Definizione: soluzione software

Una soluzione software al problema della sezione critica è una soluzione che garantisce il rispetto del requisito della mutua esclusione senza richiedere alcun supporto speciale dal sistema operativo né istruzioni hardware specifiche.

Algoritmo di Peterson

Una classica soluzione software al problema della sezione critica è l’algoritmo di Peterson.

Definizione: algoritmo di Peterson

L’algoritmo di Peterson (o soluzione di Peterson) è una soluzione al problema della sezione critica implementata a livello software tra due unità di elaborazione e .

L’algoritmo di Peterson richiede che le unità condividano i seguenti dati:

L’unità di elaborazione adotterà il seguente codice:

while (true) {
	flag[0] = true;
	turn = 1;
	while (flag[1] && turn = 1);
	// Esecuzione della sezione critica
	flag[0] = false;
	// Esecuzione della sezione non critica
}

Viceversa, l’unità di elaborazione adotterà il seguente codice:

while (true) {
	flag[1] = true;
	turn = 0;
	while (flag[0] && turn = 0);
	// Esecuzione della sezione critica
	flag[1] = false;
	// Esecuzione della sezione non critica
}

Osservazione: funzionamento dell'algoritmo di Peterson

Proviamo a capire il funzionamento dell’algoritmo di Peterson tra due unità di elaborazione e .

Prendiamo come esempio l’unità di elaborazione che:

  1. assegna a il valore per segnalare che è pronta per accedere alla sua sezione critica e

  2. attribuisce a il valore , conferendo così a la facoltà di entrare nella sua sezione critica.

Se proverà a fare la stessa cosa contemporaneamente, una delle due unità sovrascriverà il valore di dell’altra: quello che ne risulterà vincitore stabilirà chi sarà autorizzato a entrare per prima nella propria sezione critica, mentre l’altra continuerà a verificare di continuo se è arrivato il suo turno attraverso il meccanismo del busy waiting.

Dimostriamo ora la correttezza dell’algoritmo di Peterson verificando che siano rispettati i tre requisiti del problema della sezione critica.

Dimostrazione del requisito della mutua esclusione nell'algoritmo di Peterson

Per dimostrare che l’algoritmo di Peterson rispetta il requisito della mutua esclusione, osserviamo che:

Si desume da queste due osservazioni che e sono impossibilitate a eseguire con successo le rispettive istruzioni approssimativamente nello stesso momento: , infatti, può valere o , ma non entrambi. Ne consegue che non è possibile che entrambe entrino contemporaneamente: una delle due unità di elaborazione deve attendere finché l’altra non esce.

Pertanto, l’algoritmo di Peterson garantisce che solo un’unità di elaborazione alla volta possa eseguire la propria sezione critica e il requisito della mutua esclusione è rispettato.

Soluzioni più comuni oggi

Oggi le soluzioni più comuni al problema della sezione critica sono altre.

Spinlock

Una delle soluzioni più comuni oggi al problema della sezione critica è lo spinlock.

Definizione: spinlock

Lo spinlock (da spin, girare in inglese, e lock, lucchetto in inglese) è una soluzione software al problema della sezione critica che previene le Race condition attraverso l’implementazione di una variabile booleana condivisa, il , che indica se la risorsa condivisa è occupata () o meno () ed è gestita attraverso due funzioni atomiche:

  • La funzione che effettua un busy waiting su finché questa non si libera (cioè finché non risulta vero che ):
    acquire() {
    	while (lock);  // Busy waiting
    	lock = true;
    }
  • La funzione che libera il :
release() {
	lock = false;
}

e vengono rispettivamente prima e dopo l’esecuzione della sezione critica:

acquire()
// Esecuzione della sezione critica
release()
// Esecuzione della sezione non critica

Svantaggi dello spinlock

Il principale svantaggio dell’implementazione dello spinlock è che effettua un busy waiting: mentre un’unità di elaborazione si trova nella sua sezione critica, ogni altra unità che cerchi di entrare nella sezione critica deve ciclare continuamente effettuando la chiamata di .

Questo continuo ciclare è chiaramente un problema in un reale sistema multi-elaborazione in cui, per esempio, un singolo core della CPU può essere condiviso tra molte unità di elaborazione: il busy waiting spreca cicli di CPU che altre unità potrebbero essere in grado di utilizzare in modo produttivo.

Vantaggi dello spinlock

Nonostante il problema del busy waiting, gli spinlock hanno il vantaggio di non rendere necessario alcun cambio di contesto (operazione che può richiedere molto tempo) quando un’unità di elaborazione deve attendere un lock e tornano quindi utili quando si prevede che i lock verranno trattenuti per tempi brevi.

Semafori

Esaminiamo ora uno strumento più robusto in grado di comportarsi in modo simile a un spinlock, ma capace anche di fornire metodi più complessi per la sincronizzazione tra unità di esecuzione: il semaforo.

Definizione: semaforo

Un semaforo è una soluzione software al problema della sezione critica implementato attraverso una variabile intera a cui si può accedere (escludendo la sua inizializzazione) solo tramite due funzioni predefinite eseguite atomicamente, cioè in maniera indivisibile:

Una versione del semaforo che rispetta anche il requisito dell’attesa limitata è la seguente: anziché essere una semplice variabile intera, il semaforo diventa uno struct in cui sono contenuti una variabile intera e un puntatore a una lista di unità di elaborazione:

typedef struct {
	int value;
	struct process *list;
} semaphore;

Quando un’unità di elaborazione invoca la funzione e trova che il non è positivo, deve attendere, ma anziché effettuare un busy waiting si mette in coda nella e blocca se stessa con una :

wait(semaphore *s) {
	s->value--;
	if (s->value < 0) {
		// Aggiungi quest'unità di elaborazione a s->list;
		sleep();
	}
}

Quando un’altra unità di elaborazione invoca la funzione , incrementa il e se ci sono unità bloccate le preleva dalla e le riattiva con una :

signal(semaphore *s) {
	s->value++;
	if (s->value <= 0) {
		// Togli un'unità di elaborazione P da s->list;
		wakeup(P);
	}
}

I semafori trovano applicazione nel controllo dell’accesso a una data risorsa presente in un numero finito di esemplari. Dopo aver inizialmente impostato il al numero di risorse disponibili:

  • I processi che desiderino utilizzare una risorsa invocano , decrementandone così il valore, e dopo l’esecuzione di questa eseguono la propria sezione critica:
    wait(s);
    // Esecuzione della sezione critica
  • I processi che restituiscono una risorsa dopo aver eseguito la propria sezione critica, invece, invocano , incrementandone il valore:
    // Esecuzione della sezione critica
    signal(s);

Quando il semaforo vale , tutte le risorse sono occupate e i processi che ne richiedano l’uso dovranno bloccarsi fino a che il semaforo non ritorni positivo.

Osservazione: implementazione della lista dei processi bloccati

La lista dei processi che attendono a un semaforo si può facilmente realizzare con un puntatore al relativo PCB di quei processi gestiti attraverso una coda FIFO. In generale, tuttavia, si può usare qualsiasi criterio d’accodamento; il corretto uso dei semafori non dipende dal particolare criterio adottato.

Osservazione: semafori nei sistemi mono-elaborazione e multi-elaborazione

Come già abbiamo detto, le operazioni sui semafori devono essere eseguite in modo atomico: si deve garantire che nessuna coppia di processi possa eseguire operazioni e contemporaneamente sullo stesso semaforo. Si tratta di un problema della sezione critica e in un sistema mono-elaborazione lo si può risolvere semplicemente inibendo le interruzioni durante l’esecuzione di e , dal momento che rappresentano i soli elementi di disturbo: non vi sono istruzioni eseguite da altre unità di esecuzione. Finché non si riattivino le interruzioni, dando la possibilità allo scheduler di riprendere il controllo della CPU, il processo corrente continua indisturbato la sua esecuzione.

Nei sistemi multi-elaborazione sarebbe necessario disabilitare le interruzioni di tutte le unità di elaborazione, perché altrimenti le istruzioni dei diversi processi in esecuzione su unità di elaborazione distinte potrebbero interferire fra loro. Tuttavia, disabilitare le interruzioni in questo caso può essere complesso e causare un notevole calo delle prestazioni. È per questo che, per garantire l’esecuzione atomica di e , i sistemi smp devono mettere a disposizione altre tecniche di realizzazione dei lock (per esempio, CAS o spinlock).

Monitor

Benché i semafori costituiscano un meccanismo pratico ed efficace per la sincronizzazione dei processi, il loro uso scorretto può generare errori difficili da individuare, in quanto si manifestano solo in presenza di particolari sequenze di esecuzione che non si verificano sempre, ossia a causa di race condition.

Altri errori nell’uso dei semafori possono nascere invece da un involontario errore di programmazione o essere causato dalla mancata collaborazione del programmatore. Per esempio, supponiamo che un processo capovolga l’ordine in cui sono eseguite le istruzioni e , in questo modo:

signal(s);
// Esecuzione della sezione critica
wait(s);

In questa situazione, numerosi processi possono eseguire le proprie sezioni critiche allo stesso tempo, violando il requisito della mutua esclusione.

Un altro errore potrebbe derivare dalla sostituzione di con :

wait(s);
// Esecuzione della sezione critica
wait(s);

In questo caso, il processo si bloccherà in modo permanente alla seconda chiamata , poiché il semaforo non è più disponibile.

Supponiamo, infine, che un processo ometta la , la o entrambi: in questo caso si viola il requisito della mutua esclusione oppure il processo si blocca indefinitamente (cioè avviene un’attesa indefinita).

Questi esempi chiariscono come sia facile incorrere in errori quando i programmatori utilizzano i semafori in maniera scorretta nel risolvere il problema delle sezioni critiche. Una strategia per gestire tali errori consiste nell’incorporare questi strumenti di sincronizzazione in costrutti ad alto livello che li astragga, come il monitor.

Definizione: monitor

Un monitor è un ADT che incapsula variabili locali private condivise tra più unità di esecuzione e permette di accedervi solo tramite funzioni che ne assicurano l’accesso solo in mutua esclusione:

typedef monitor {
	var v1;
	var v2;
	// Dichiarazione di altre variabili
	var vn;
	function f1();
	function f2();
	// Dichiarazione di altre funzioni
	function fn();
}

Esempio di uso dei monitor

Un esempio di uso dei monitor è

monitor conto_corrente {
  double saldo := 0.0
  
  procedure preleva(double importo) {
    if importo < 0.0 then error "L'importo del prelievo deve essere un numero positivo"
    else if saldo < importo then error "Fondi insufficienti per il prelievo"
    else saldo:= saldo - importo
  }
  
  procedure versa(double importo) {
    if importo < 0.0 then error "L'importo del versamento deve essere un numero positivo"
    else saldo:= saldo + importo
  }
 
  double function saldo() {
    return saldo
  }
}

Variabili di condizione

In questa definizione di monitor appena vista non basta per assicurare il suo corretto uso: per esempio, riprendiamo il problema del produttore-consumatore. Se dovessimo usare un monitor nella sua implementazione, avremmo che il consumatore può entrare nel monitor anche quando il buffer è vuoto, ma non potendo consumare rimane in attesa finché non trova qualcosa da consumare. Tuttavia, dato che il monitor è già occupato dal consumatore, il produttore non può accedervi e di conseguenza non può produrre nuovi elementi: ciò causa uno stallo tra i due.

Questo problema può essere arginato dall’uso di particolari variabili, dette variabili di condizione, che permettono al consumatore di bloccarsi, rilasciando il monitor e permettendo al produttore di accedervi.

Definizione: variabile di condizione

Una variabile di condizione (in inglese condition variable) è una variabile presente in un monitor che consente a un’unità di esecuzione, mentre si trova all’interno del monitor, di sospendere la propria esecuzione fino a quando una certa condizione logica sui dati del monitor non risulta vera, permettendo temporaneamente ad altre unità di esecuzione l’accesso al monitor.

Le uniche funzioni eseguibili su una variabile di condizione sono:

  • la con cui l’unità di esecuzione che la invoca rimane sospesa finché un’altra non invoca
  • la che risveglia una delle unità di esecuzione rimaste sospese dopo l’esecuzione della .

Osservazione: differenza tra la dei semafori e dei monitor

Se non esistono unità di esecuzione sospese dopo l’esecuzione di una , l’esecuzione di una in un monitor non ha alcun effetto, vale a dire che lo stato di resta immutato, come se la funzione non fosse stata eseguita.

Ciò non equivale alla di un semaforo, poiché questa influisce sempre sullo stato del semaforo incrementando il suo .

Facciamo finta che un processo abbia invocato l’istruzione e che esista un processo sospeso associato alla variabile di condizione (e cioè che ha eseguito ). Chiaramente, se al processo sospeso si permette di riprendere l’esecuzione, il processo segnalante è costretto ad attendere, altrimenti e sarebbero contemporaneamente attivi all’interno del monitor. Sussistono allora due possibilità la signal-and-wait e la signal-and-continue.

Definizione: signal-and-wait

Signal-and-wait è una politica di gestione dei monitor nel caso dell’uso di variabili di condizione in cui l’unità di esecuzione che esegue la attende che l’altra unità risvegliata lasci il monitor per continuare la propria esecuzione. È contrapposta alla politica signal-and-continue.

Definizione: signal-and-continue

Signal-and-continue è una politica di gestione dei monitor nel caso dell’uso di variabili di condizione in cui l’unità di esecuzione che esegue la continua la propria esecuzione, lasciando che l’altra unità risvegliata aspetti il suo turno. È contrapposta alla politica signal-and-wait.

Osservazione: realizzazione di un monitor per mezzo di semafori

Si può realizzare un monitor per mezzo di semafori.

A ogni monitor si associa un semaforo , il cui è inizializzato a , per garantire la mutua esclusione; un processo deve eseguire prima di entrare nel monitor e dopo aver lasciato il monitor.

Useremo nella nostra implementazione la politica signal-and-wait. Poiché un processo che esegue una deve attendere finché il processo risvegliato si metta in attesa o lasci il monitor, introduciamo un altro semaforo inizializzato a , su cui i processi che eseguono una possono autosospendersi. Per contare i processi sospesi al semaforo , usiamo una variabile intera :

semaphore s = 1;
semaphore next = 0;
int next_count = 0;

Quindi, ogni funzione esterna al monitor si sostituisce col seguente codice:

wait(s);
// Corpo di f()
if (counter > 0)
	signal(next);
else
	signal(s);

In questo modo si assicura la mutua esclusione all’interno del monitor.

A questo punto si può descrivere la realizzazione delle variabili di condizione: per ogni variabile di condizione si introducono un semaforo e una variabile intera , entrambi inizializzati a . Abbiamo che:

  • L’operazione si può realizzare come segue:
    x_counter++;
    if (counter > 0)
    	signal(next);
    else
    	signal(s);
    wait(x_semaphore);
    x_counter--;
  • L’operazione si può realizzare come segue:
    if (x_counter > 0) {
    	counter++;
    	signal(x_semaphore);
    	wait(next);
    	counter--;
    }

Ordine di ripresa

Se più unità di esecuzione in un monitor sono sospese alla variabile di condizione e se qualcun’altra esegue l’operazione , è necessario stabilire quale tra le unità sospese debba essere riattivata per prima.

Una semplice soluzione consiste nell’usare una politica FIFO, secondo cui l’unità di esecuzione che attende da più tempo viene ripreso per primo. Tuttavia, in molti casi ciò non risulta adeguato, ragion per cui in questi casi si può implementare una priorità attraverso un numero di priorità.

Definizione: numero di priorità

Un numero di priorità è un’espressione il cui risultato è un valore intero, passato come parametro alla invocata su una variabile di condizione all’interno di un monitor:

Il valore di viene valutato al momento dell’esecuzione della e viene poi associato alla relativa unità di esecuzione sospesa che l’ha eseguita. Quando si esegue , si riattiva l’unità cui è associato il numero di priorità più basso.

Valutazione delle soluzioni

Abbiamo descritto diversi strumenti di sincronizzazione utilizzabili per risolvere il problema della sezione critica. Con una corretta implementazione e un utilizzo appropriato, questi strumenti possono essere efficacemente impiegati per garantire la mutua esclusione e affrontare problemi di liveness. Con la diffusione di programmi concorrenti in grado di sfruttare la potenza dei moderni sistemi multicore viene prestata sempre maggior attenzione alle prestazioni degli strumenti di sincronizzazione. Cercare di identificare quale strumento utilizzare e quando, tuttavia, può essere una sfida improba. In questo paragrafo presentiamo alcune semplici strategie per determinare quando utilizzare specifici strumenti di sincronizzazione.

Le soluzioni hardware descritte nel Paragrafo 6.4 sono considerate di livello molto basso e vengono tipicamente utilizzate come base per la costruzione di altri strumenti di sincronizzazione, come i lock mutex. Tuttavia, ci si è concentrati di recente sull’uso dell’istruzione cas per costruire algoritmi privi di lock che forniscono protezione dalle race condition senza l’overhead dei lock. Sebbene queste soluzioni lock-free stiano guadagnando popolarità grazie al basso overhead e alla loro scalabilità, gli algoritmi sono spesso difficili da sviluppare e testare.

Gli approcci basati su cas sono considerati ottimistici: prima si aggiorna fiduciosamente una variabile e poi si utilizza il rilevamento delle collisioni per verificare se un altro thread stia aggiornando la stessa variabile contemporaneamente. In tal caso, si riprova ripetutamente l’operazione fino a quando la variabile non viene aggiornata correttamente e senza conflitti. Il lock mutex, al contrario, è considerato una strategia pessimistica: si presuppone che un altro thread stia aggiornando contemporaneamente la variabile e in modo pessimistico si acquisisce il lock prima di apportare qualsiasi aggiornamento.

Le seguenti linee guida identificano delle regole generali per distinguere le prestazioni della sincronizzazione basata su cas e della sincronizzazione tradizionale (come i lock mutex e i semafori) al variare del livello di contesa:

  • Nessuna contesa. Sebbene entrambe le opzioni siano generalmente veloci, la protezione cas sarà leggermente più veloce della sincronizzazione tradizionale.
  • Contesa moderata. La protezione cas sarà più veloce e in alcuni casi molto più veloce rispetto alla sincronizzazione tradizionale.
  • Alta contesa. Con carichi molto elevati, la sincronizzazione tradizionale sarà in definitiva più veloce della sincronizzazione basata su cas.

La contesa moderata è particolarmente interessante da esaminare. In questo scenario, l’operazione cas ha esito positivo per la maggior parte del tempo e, in caso di errore, itererà nel ciclo mostrato nella Figura 6.8 solo poche volte prima di avere successo. Per contro, con i lock mutex qualsiasi tentativo di acquisire un lock conteso comporterà l’esecuzione di un codice più complicato e oneroso che sospende un thread e lo colloca in una coda di wait, richiedendo un cambio di contesto verso un altro thread.

La scelta di un meccanismo per affrontare le race condition può anche influire notevolmente sulle prestazioni del sistema. Per esempio, gli interi atomici sono molto più leggeri dei lock tradizionali e sono generalmente più appropriati dei lock mutex o dei semafori per singoli aggiornamenti di variabili condivise come i contatori. Ciò è visibile anche nella progettazione dei sistemi operativi, in cui gli spinlock vengono utilizzati su sistemi multiprocessore quando i lock vengono mantenuti per brevi periodi. In generale, i lock mutex sono più semplici e comportano meno overhead rispetto ai semafori; sono preferibili ai semafori binari per proteggere l’accesso a una sezione critica. Tuttavia, in alcuni casi, come nel controllo dell’accesso a un numero limitato di risorse, un semaforo contatore è generalmente più appropriato di un lock mutex. Analogamente, in alcuni casi, un lock lettore-scrittore può essere preferito a un lock mutex, in quanto consente un maggior grado di concorrenza (cioè più lettori).

L’attrattiva di strumenti di livello superiore come monitor e variabili condizionali si basa sulla loro semplicità e facilità d’uso. Tuttavia, tali strumenti potrebbero avere un overhead significativo e, a seconda della loro implementazione, potrebbero essere meno scalabili a situazioni con elevate contese.

Fortunatamente sono in corso molte ricerche per sviluppare strumenti scalabili ed efficienti che rispondano alle esigenze della programmazione concorrente. Alcuni esempi comprendono:

  • progettazione di compilatori che generano codice più efficiente;
  • sviluppo di linguaggi che forniscono supporto per la programmazione concorrente;
  • miglioramento delle prestazioni delle librerie e delle api esistenti.

Nel prossimo capitolo esamineremo in che modo i vari sistemi operativi e le api disponibili agli sviluppatori implementano gli strumenti di sincronizzazione presentati in questo capitolo.