Risposte:
Tratto da http://en.wikipedia.org/wiki/Deadlock :
Nel calcolo simultaneo, un deadlock è uno stato in cui ciascun membro di un gruppo di azioni è in attesa che un altro membro rilasci un blocco
Un livelock è simile a un deadlock, tranne per il fatto che gli stati dei processi coinvolti nel livelock cambiano costantemente l'uno rispetto all'altro, nessuno progredendo. Livelock è un caso speciale di fame di risorse; la definizione generale afferma solo che un processo specifico non sta progredendo.
Un esempio del mondo reale di livelock si verifica quando due persone si incontrano in uno stretto corridoio, e ognuna cerca di essere educata spostandosi di lato per lasciare passare l'altra, ma finiscono per oscillare da un lato all'altro senza fare alcun progresso perché entrambi si muovono ripetutamente allo stesso modo allo stesso tempo.
Livelock è un rischio con alcuni algoritmi che rilevano e ripristinano da deadlock. Se interviene più di un processo, è possibile attivare ripetutamente l'algoritmo di rilevamento dei deadlock. Questo può essere evitato assicurando che solo un processo (scelto casualmente o per priorità) agisca.
Un thread agisce spesso in risposta all'azione di un altro thread. Se anche l'azione dell'altro thread è una risposta all'azione di un altro thread, potrebbe verificarsi un livelock.
Come nel caso del deadlock, i thread con livelock non sono in grado di fare ulteriori progressi . Tuttavia, i thread non sono bloccati : sono semplicemente troppo occupati a rispondere l'un l'altro per riprendere il lavoro . Questo è paragonabile a due persone che tentano di passarsi l'un l'altro in un corridoio: Alphonse si sposta alla sua sinistra per far passare Gaston, mentre Gaston si sposta alla sua destra per far passare Alphonse. Vedendo che si stanno ancora bloccando a vicenda, Alphonse si sposta alla sua destra, mentre Gaston si sposta alla sua sinistra. Si stanno ancora bloccando a vicenda, e così via ...
La differenza principale tra livelock e deadlock è che i thread non verranno bloccati, ma cercheranno di rispondersi continuamente.
In questa immagine, entrambi i cerchi (thread o processi) proveranno a dare spazio all'altro spostandosi a sinistra e a destra. Ma non possono muoversi oltre.
Tutto il contenuto e gli esempi qui provengono da
Sistemi operativi: principi interni e di progettazione
William Stallings
8º edizione
Deadlock : una situazione in cui due o più processi non sono in grado di procedere perché ognuno è in attesa che l'altro faccia qualcosa.
Ad esempio, considera due processi, P1 e P2 e due risorse, R1 e R2. Supponiamo che ogni processo abbia bisogno di accedere ad entrambe le risorse per svolgere parte della sua funzione. Quindi è possibile avere la seguente situazione: il sistema operativo assegna R1 a P2 e R2 a P1. Ogni processo è in attesa di una delle due risorse. Nessuno dei due rilascerà la risorsa che possiede già fino a quando non ha acquisito l'altra risorsa ed eseguito la funzione che richiede entrambe le risorse. I due processi sono bloccati
Livelock : una situazione in cui due o più processi cambiano continuamente i loro stati in risposta ai cambiamenti negli altri processi senza svolgere alcun lavoro utile:
Fame : una situazione in cui un pianificatore esegue un processo a tempo indeterminato indefinitamente; sebbene sia in grado di procedere, non viene mai scelto.
Supponiamo che tre processi (P1, P2, P3) richiedano ciascuno un accesso periodico alla risorsa R. Considera la situazione in cui P1 è in possesso della risorsa e sia P2 che P3 sono in ritardo, in attesa di quella risorsa. Quando P1 esce dalla sua sezione critica, P2 o P3 dovrebbero avere accesso a R. Supponi che il sistema operativo conceda l'accesso a P3 e che P1 richieda nuovamente l'accesso prima che P3 completi la sua sezione critica. Se il sistema operativo concede l'accesso a P1 dopo che P3 è terminato e successivamente concede alternativamente l'accesso a P1 e P3, a P2 può essere indefinitamente negato l'accesso alla risorsa, anche se non si verifica una situazione di deadlock.
APPENDICE A - TEMI IN CONCORRENZA
Esempio di deadlock
Se entrambi i processi impostano i loro flag su true prima che uno dei due abbia eseguito l'istruzione while, ognuno penserà che l'altro sia entrato nella sua sezione critica, causando deadlock.
/* PROCESS 0 */
flag[0] = true; // <- get lock 0
while (flag[1]) // <- is lock 1 free?
/* do nothing */; // <- no? so I wait 1 second, for example
// and test again.
// on more sophisticated setups we can ask
// to be woken when lock 1 is freed
/* critical section*/; // <- do what we need (this will never happen)
flag[0] = false; // <- releasing our lock
/* PROCESS 1 */
flag[1] = true;
while (flag[0])
/* do nothing */;
/* critical section*/;
flag[1] = false;
Esempio di Livelock
/* PROCESS 0 */
flag[0] = true; // <- get lock 0
while (flag[1]){
flag[0] = false; // <- instead of sleeping, we do useless work
// needed by the lock mechanism
/*delay */; // <- wait for a second
flag[0] = true; // <- and restart useless work again.
}
/*critical section*/; // <- do what we need (this will never happen)
flag[0] = false;
/* PROCESS 1 */
flag[1] = true;
while (flag[0]) {
flag[1] = false;
/*delay */;
flag[1] = true;
}
/* critical section*/;
flag[1] = false;
[...] considera la seguente sequenza di eventi:
Questa sequenza potrebbe essere estesa indefinitamente e nessuno dei due processi potrebbe entrare nella sua sezione critica. A rigor di termini, questo non è un deadlock , perché qualsiasi alterazione della velocità relativa dei due processi interromperà questo ciclo e consentirà di entrare nella sezione critica. Questa condizione è denominata livelock . Ricorda che il deadlock si verifica quando un insieme di processi desidera accedere alle loro sezioni critiche ma nessun processo può avere successo. Con livelock , ci sono possibili sequenze di esecuzioni che hanno successo, ma è anche possibile descrivere una o più sequenze di esecuzione in cui nessun processo entra nella sua sezione critica.
Non più contenuto del libro.
E gli spinlock?
Spinlock è una tecnica per evitare il costo del meccanismo di blocco del sistema operativo. In genere si dovrebbe fare:
try
{
lock = beginLock();
doSomething();
}
finally
{
endLock();
}
Un problema inizia ad apparire quando beginLock()
costa molto di più doSomething()
. In termini molto esagerati, immagina cosa succede quando beginLock
costa 1 secondo, ma doSomething
costa solo 1 millisecondo.
In questo caso, se hai aspettato 1 millisecondo, eviterai di essere ostacolato per 1 secondo.
Perché beginLock
costerebbe così tanto? Se il blocco è gratuito non costa molto (vedi https://stackoverflow.com/a/49712993/5397116 ), ma se il blocco non è libero il sistema operativo "congela" il tuo thread, imposta un meccanismo per riattivarti quando il blocco viene liberato, quindi svegliarti di nuovo in futuro.
Tutto questo è molto più costoso di alcuni anelli che controllano la serratura. Ecco perché a volte è meglio fare uno "spinlock".
Per esempio:
void beginSpinLock(lock)
{
if(lock) loopFor(1 milliseconds);
else
{
lock = true;
return;
}
if(lock) loopFor(2 milliseconds);
else
{
lock = true;
return;
}
// important is that the part above never
// cause the thread to sleep.
// It is "burning" the time slice of this thread.
// Hopefully for good.
// some implementations fallback to OS lock mechanism
// after a few tries
if(lock) return beginLock(lock);
else
{
lock = true;
return;
}
}
Se la tua implementazione non è attenta, puoi cadere su livelock, spendendo tutta la CPU sul meccanismo di blocco.
Vedi anche:
https://preshing.com/20120226/roll-your-own-lightweight-mutex/
La mia implementazione del blocco spin è corretta e ottimale?
Riepilogo :
Deadlock : situazione in cui nessuno progredisce, non fa nulla (dormire, aspettare ecc.). L'utilizzo della CPU sarà basso;
Livelock : situazione in cui nessuno progredisce, ma la CPU viene spesa nel meccanismo di blocco e non nel calcolo;
La fame: situazione in cui una procura non ha mai la possibilità di correre; per pura sfortuna o per parte della sua proprietà (bassa priorità, ad esempio);
Spinlock : tecnica per evitare il costo in attesa del rilascio della serratura.
DEADLOCK Deadlock è una condizione in cui un'attività attende indefinitamente condizioni che non possono mai essere soddisfatte - l'attività rivendica il controllo esclusivo sulle risorse condivise - l'attività contiene risorse in attesa di altre risorse da rilasciare - le attività non possono essere costrette a spegnere le risorse - un'attesa circolare la condizione esiste
LIVELOCK Le condizioni di Livelock possono sorgere quando due o più attività dipendono e utilizzano alcune risorse che causano una condizione di dipendenza circolare in cui tali attività continuano a funzionare per sempre, bloccando così l'esecuzione di tutte le attività di livello di priorità inferiore (queste attività di priorità inferiore presentano una condizione chiamata fame)
Forse questi due esempi ti illustrano la differenza tra un deadlock e un livelock:
Esempio Java per un deadlock:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DeadlockSample {
private static final Lock lock1 = new ReentrantLock(true);
private static final Lock lock2 = new ReentrantLock(true);
public static void main(String[] args) {
Thread threadA = new Thread(DeadlockSample::doA,"Thread A");
Thread threadB = new Thread(DeadlockSample::doB,"Thread B");
threadA.start();
threadB.start();
}
public static void doA() {
System.out.println(Thread.currentThread().getName() + " : waits for lock 1");
lock1.lock();
System.out.println(Thread.currentThread().getName() + " : holds lock 1");
try {
System.out.println(Thread.currentThread().getName() + " : waits for lock 2");
lock2.lock();
System.out.println(Thread.currentThread().getName() + " : holds lock 2");
try {
System.out.println(Thread.currentThread().getName() + " : critical section of doA()");
} finally {
lock2.unlock();
System.out.println(Thread.currentThread().getName() + " : does not hold lock 2 any longer");
}
} finally {
lock1.unlock();
System.out.println(Thread.currentThread().getName() + " : does not hold lock 1 any longer");
}
}
public static void doB() {
System.out.println(Thread.currentThread().getName() + " : waits for lock 2");
lock2.lock();
System.out.println(Thread.currentThread().getName() + " : holds lock 2");
try {
System.out.println(Thread.currentThread().getName() + " : waits for lock 1");
lock1.lock();
System.out.println(Thread.currentThread().getName() + " : holds lock 1");
try {
System.out.println(Thread.currentThread().getName() + " : critical section of doB()");
} finally {
lock1.unlock();
System.out.println(Thread.currentThread().getName() + " : does not hold lock 1 any longer");
}
} finally {
lock2.unlock();
System.out.println(Thread.currentThread().getName() + " : does not hold lock 2 any longer");
}
}
}
Uscita campione:
Thread A : waits for lock 1
Thread B : waits for lock 2
Thread A : holds lock 1
Thread B : holds lock 2
Thread B : waits for lock 1
Thread A : waits for lock 2
Esempio Java per un livelock:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LivelockSample {
private static final Lock lock1 = new ReentrantLock(true);
private static final Lock lock2 = new ReentrantLock(true);
public static void main(String[] args) {
Thread threadA = new Thread(LivelockSample::doA, "Thread A");
Thread threadB = new Thread(LivelockSample::doB, "Thread B");
threadA.start();
threadB.start();
}
public static void doA() {
try {
while (!lock1.tryLock()) {
System.out.println(Thread.currentThread().getName() + " : waits for lock 1");
Thread.sleep(100);
}
System.out.println(Thread.currentThread().getName() + " : holds lock 1");
try {
while (!lock2.tryLock()) {
System.out.println(Thread.currentThread().getName() + " : waits for lock 2");
Thread.sleep(100);
}
System.out.println(Thread.currentThread().getName() + " : holds lock 2");
try {
System.out.println(Thread.currentThread().getName() + " : critical section of doA()");
} finally {
lock2.unlock();
System.out.println(Thread.currentThread().getName() + " : does not hold lock 2 any longer");
}
} finally {
lock1.unlock();
System.out.println(Thread.currentThread().getName() + " : does not hold lock 1 any longer");
}
} catch (InterruptedException e) {
// can be ignored here for this sample
}
}
public static void doB() {
try {
while (!lock2.tryLock()) {
System.out.println(Thread.currentThread().getName() + " : waits for lock 2");
Thread.sleep(100);
}
System.out.println(Thread.currentThread().getName() + " : holds lock 2");
try {
while (!lock1.tryLock()) {
System.out.println(Thread.currentThread().getName() + " : waits for lock 1");
Thread.sleep(100);
}
System.out.println(Thread.currentThread().getName() + " : holds lock 1");
try {
System.out.println(Thread.currentThread().getName() + " : critical section of doB()");
} finally {
lock1.unlock();
System.out.println(Thread.currentThread().getName() + " : does not hold lock 1 any longer");
}
} finally {
lock2.unlock();
System.out.println(Thread.currentThread().getName() + " : does not hold lock 2 any longer");
}
} catch (InterruptedException e) {
// can be ignored here for this sample
}
}
}
Uscita campione:
Thread B : holds lock 2
Thread A : holds lock 1
Thread A : waits for lock 2
Thread B : waits for lock 1
Thread B : waits for lock 1
Thread A : waits for lock 2
Thread A : waits for lock 2
Thread B : waits for lock 1
Thread B : waits for lock 1
Thread A : waits for lock 2
Thread A : waits for lock 2
Thread B : waits for lock 1
...
Entrambi gli esempi costringono i fili ad acquisire le serrature in diversi ordini. Mentre il deadlock attende l'altro lock, il livelock non aspetta davvero - cerca disperatamente di acquisire il lock senza la possibilità di ottenerlo. Ogni tentativo consuma cicli di CPU.
Immagina di avere il thread A e il thread B. Sono entrambi synchronised
sullo stesso oggetto e all'interno di questo blocco c'è una variabile globale che entrambi stanno aggiornando;
static boolean commonVar = false;
Object lock = new Object;
...
void threadAMethod(){
...
while(commonVar == false){
synchornized(lock){
...
commonVar = true
}
}
}
void threadBMethod(){
...
while(commonVar == true){
synchornized(lock){
...
commonVar = false
}
}
}
Quindi, quando il thread A entra nel while
loop e mantiene il blocco, fa quello che deve fare e imposta il commonVar
to true
. Poi filo B entra, entra nel while
ciclo e poiché commonVar
è true
ora, è in grado di mantenere il blocco. Lo fa, esegue il synchronised
blocco e commonVar
torna a false
. Ora, il thread A ottiene nuovamente la sua nuova finestra della CPU, stava per uscire dal while
loop ma il thread B lo ha appena ripristinato false
, quindi il ciclo si ripete di nuovo. Le discussioni fanno qualcosa (quindi non sono bloccate nel senso tradizionale) ma praticamente per niente.
Forse è anche bello ricordare che il livelock non deve necessariamente apparire qui. Suppongo che lo scheduler favorisca l'altro thread una volta synchronised
terminato l'esecuzione del blocco. Il più delle volte, penso che sia un'aspettativa difficile da colpire e dipende da molte cose che accadono sotto il cofano.