Abbiamo due processi paralleli eseguiti in maniera asincrona e una porzione di codice, chiamata sezione critica, per la quale vogliamo garantire mutua esclusione: al più un processo per volta deve aver accesso alla sezione critica e, in nessun caso, nessun processo deve ritrovarsi bloccato (deadlock e starvation) senza mai poter entrare o uscire dalla tale sezione. L'esempio classico riguarda l'utilizzo di risorse condivise: due processi vogliono scrivere sullo stesso file, ma se lo fanno contemporaneamente il risultato sarà incomprensibile.
Siamo negli anni '80, quindi non abbiamo hardware facilities come le istruzioni atomiche test-and-set e compare-and-swap. Se un processo assegna un valore ad una variabile quando un altro processo controlla il suo valore, il risultato è impredicibile. La prima soluzione a questo problema risale agli anni '60 (
algoritmo di Dekker), usava un ciclo innestato e non è semplice da capire. Per circa 15 anni hanno provato a fornire semplificazioni di vario genere ma, in generale, gli algoritmi concorrenti sono difficili da analizzare correttamente e molte delle soluzioni proposte si sono poi rivelate sbagliate. L'algoritmo di Peterson è importante in quanto fornisce la prima soluzione semplice (semplice da scrivere, ma soprattutto semplice da dimostrare) per il problema della mutua esclusione.
Codice:
int turn;
bool F1 = false;
bool F2 = false;
P1 | P2
------------------------------------------------+------------------------------------------------
F1 = true; | F2 = true;
turn = 2; | turn = 1;
while (F2 == true && turn == 2) { /* wait */ } | while (F1 == true && turn == 1) { /* wait */ }
critical_section(); | critical_section();
F1 = false; | F2 = false;
Giusto per mettere le cose in chiaro, la condizione del while loop
non viene calcolata in modo atomico quindi:
- P1 controlla la prima metà del while,
F2 == true
, e stabilisce che la condizione è vera;
- P2 esegue
F2 = false;
;
- P1 controlla la seconda metà del while,
turn == 1
, e stabilisce che la condizione è vera; e
- P1 entra nel while senza accorgersi che la prima condizione è diventata falsa
è uno degli scenari plausibili.
Dimostriamo che la mutua esclusione è garantita supponendo, per assurdo, che entrambi i processi si trovano contemporaneamente all'interno della sezione critica. Ovvero, sia F1 che F2 sono true quando in un certo istante di tempo P1 ha trovato turn a 1 e, in un altro istante di tempo, P2 ha trovato turn a 2. Questo scenario è impossibile perché nel momento in cui P1 trova turn a 1, l'assegnamento
turn = 2
fatto da P1 è già stato sovrascritto da P2 e quindi non potrà essere usato a sua volta per sovrascrivere l'assegnamento
turn = 2
fatto da P2. È banalmente vero il vice versa.
Dimostriamo che nessun processo viene tagliato fuori (starvation) per sempre. Assumendo che P1 sia in possibile starvation nel ciclo while, ci ritroviamo in una di queste condizioni:
P2 non è interessato alla sezione critica: F2 è false perché P2 è già uscito dalla sezione critica o non ha mai provato ad entrarci, quindi P1 si sblocca immediatamente.
P2 è all'interno della sezione critica: P2 eseguirà F2 = false
non appena avrà terminato di usare la sezione critica, quindi P1 si sblocca dopo un lasso di tempo finito.
P2 è anche lui in busy waiting nel ciclo while: visto che la variabile turn o vale 1 o vale 2, uno dei due processi è destinato a sbloccarsi immediatamente.
L'ultima condizione dimostra anche l'assenza di deadlock: quando entrambi sono in attesa non ci sono assegnamenti, e dato che turn può assumere solo due valori uno (e solo uno) non è in condizione di aspettare ulteriormente.
Mi rendo conto che è un po' una spiegazione "alla wikipedia", ma a domande vaghe non possono che esserci risposte generiche. Se mi chiarisci cosa non hai capito, o se provi a spiegarmi tutto l'algoritmo a parole tue, forse riesco a darti un aiuto più mirato.