Ho letto delle tecniche lock-free, come Confronta-e-swap e sfruttando le classi Interlocked e SpinWait per ottenere la sincronizzazione dei thread senza bloccare.Locks vs Compare-and-swap
Ho eseguito alcuni test personali, in cui ho semplicemente molti thread che tentano di aggiungere un carattere a una stringa. Ho provato a utilizzare i normali lock
e confrontare-e-swap. Sorprendentemente (almeno per me), i lucchetti hanno mostrato risultati molto migliori rispetto all'utilizzo di CAS.
Ecco la versione CAS del mio codice (basata su this). Si segue un modello della copia> Modifica-> swap:
private string _str = "";
public void Append(char value)
{
var spin = new SpinWait();
while (true)
{
var original = Interlocked.CompareExchange(ref _str, null, null);
var newString = original + value;
if (Interlocked.CompareExchange(ref _str, newString, original) == original)
break;
spin.SpinOnce();
}
}
E il più semplice (e più efficiente) versione di blocco:
private object lk = new object();
public void AppendLock(char value)
{
lock (lk)
{
_str += value;
}
}
Se provo l'aggiunta di 50.000 caratteri, la versione CAS prende 1,2 secondi e la versione di blocco 700ms (media). Per 100k caratteri, prendono rispettivamente 7 secondi e 3,8 secondi. Questo è stato eseguito su un quad-core (i5 2500k).
Avevo il sospetto che il motivo per cui CAS stava visualizzando questi risultati era dovuto al fatto che l'ultimo passaggio di "scambio" era fallito. Avevo ragione. Quando provo ad aggiungere 50k char (50k swap riusciti), sono riuscito a contare tra 70k (scenario del caso migliore) e quasi 200k (scenario peggiore) tentativi falliti. Nella peggiore delle ipotesi, 4 tentativi su 5 falliscono.
Quindi le mie domande sono:
- Che cosa mi manca? CAS non dovrebbe dare risultati migliori? Dov'è il vantaggio?
- Perché esattamente e quando CAS è un'opzione migliore? (So che è stato chiesto questo, ma non riesco a trovare alcuna risposta soddisfacente che spiega anche il mio scenario specifico).
Sono a conoscenza del fatto che le soluzioni che utilizzano CAS, anche se difficili da codificare, si adattano molto meglio e funzionano meglio delle serrature al crescere della contesa. Nel mio esempio, le operazioni sono molto piccole e frequenti, il che significa alta contesa e alta frequenza. Allora perché i miei test mostrano il contrario?
Suppongo che operazioni più lunghe peggiorerebbero ulteriormente il caso -> il tasso di sostituzione "swap" aumenterebbe ancora di più.
PS: questo è il codice che ho usato per eseguire i test:
Stopwatch watch = Stopwatch.StartNew();
var cl = new Class1();
Parallel.For(0, 50000, i => cl.Append('a'));
var time = watch.Elapsed;
Debug.WriteLine(time.TotalMilliseconds);
No, non si misura il tempo di esecuzione di CAS, ma principalmente il tempo di esecuzione della stringa di confronto. La classe Interlocked sfortunatamente non ha un'operazione di lettura-modifica-scrittura atomica per i tipi di riferimento (è quello che stai facendo essenzialmente nell'esempio "lock" senza fare affidamento sui confronti delle stringhe.) – elgonzo
La tua soluzione senza blocco sta facendo più lavoro della serratura versione. Innanzitutto, l'iniziale 'CompareExchange' per leggere il valore esistente è eccessivo, l'esecuzione di una lettura volatile (' Thread.VolatileRead') darà lo stesso risultato senza un sovraccarico. Secondo, ogni tentativo di aggiornamento all'interno del ciclo duplicherà il valore "corrente" della stringa e aggiungerà i nuovi valori. Non puoi fare nulla per questo, ma la versione di blocco non soffre di questo problema. È la copia stringa che molto probabilmente causa la maggior parte della differenza di orario. – William
Per noi semplici mortali, continua ad usare le serrature esistenti piuttosto che provare a rotolare le tue. Il multithreading è abbastanza difficile senza dover affrontare i problemi [ABA] (http://en.wikipedia.org/wiki/ABA_problem). – William