SpinLock e SpinWait

spinlock spinwait

Questo post è un seguito al mio precedente post sul blocking e spinning e vuole essere un piccolo approfondimento sulle nuove struct SpinLock e SpinWait di NET 4.

Come ho scritto nel post, in caso di attese molto brevi lo spinning può essere preferibile al blocking in quanto evita overhead di context switch.

Gli oggetti SpinLock e SpinWait sono state pensate esattamente per questo caso.

E’ importante sottolineare che tali oggetti non sono classi ma sono struct. Questa scelta è stata presa per fare in modo che siano sullo stack senza mai passare per lo heap e conseguentemente eliminare il costo di allocazione e garbage collection.

SpinLock

Lo struct SpinLock permette di mettere in pausa un Thread in caso di accesso a risorse condivise utilizzando lo spinning, quindi facendogli fare del lavoro inutile in loop.

Viene utilizzato negli stessi casi del costrutto lock (anche se in questo caso non ho la sintassi comoda), quindi quando voglio evitare un accesso contemporaneo alla stessa risorsa.

var spinLock = new SpinLock(true);
var lockTaken = false;
try
{
    // lockTaken rimane a false dopo aver chiamato Enter se e solo se
    // il metodo Enter lancia una Eccezione e il lock non era stato ancora preso da nessuno
    spinLock.Enter(ref lockTaken);
    // Accesso a risorse critiche...
}
finally
{
    // Esco dallo SpinLock se lo ho preso
    if (lockTaken)
        spinLock.Exit();
}

SpinWait

Lo struct SpinWait permette di mettere in pausa un Thread utilizzando lo spinning, quindi facendogli fare del lavoro inutile in loop.

Effettuando un wait con spinning usando un classico while

while (!condition);

può portare a numerosi problemi:

  • Il thread consumerà tutti i core della CPU fino a che condition non sarà true;
  • Gli altri Thread dell’applicazione rallenteranno (starvation);
  • Perfino il Thread che dovrà mettere condition a true rallenterà, portando ad un loop di rallentamento orribile (chiamato priority inversion);
  • In caso di CPU single core la priority inversion è pressoché certa.

La struct SpinWait risolve questi problemi in due modi:

  • Limita il numero di spinning CPU intensive ad un massimo numero di iterazioni: dopo aver raggiunto tale numero massimo fa lo yield del time slice a lui assegnato utilizzando Thread.Sleep o Thread.Yield (ricordo che il thread scheduler del sistema operativo assegna dei time slice ad ogni thread e rapidamente continua a cambiare l’esecuzione, aumento inoltre il parallelismo in base al numero di CPU a disposizione);
  • Rileva se sta lavorando su una CPU single core e in quel caso fa lo yield ad ogni ciclo.

SpinWait si può utilizzare con la sua classe statica in questo modo:

SpinWait.SpinUntil(() => myPredicate(), 1000)

che di fatto aspetta che myPredicate() diventi true per al massimo 1000 ms.

Aspettare facendo spinning per 1000ms non ha senso, potrebbe essere una idea più interessante provare lo spinning per, esempio, 10ms e successivamente un thread.sleep classico.

Una alternativa è utilizzare il metodo SpinOnce() all’interno di un while che esce su una determinata condition

var sw = new SpinWait();
while (!condition)
{
    sw.SpinOnce();
}

Oppure è possibile utilizzare la property NextSpinWillYield per poter essere intenzionale nel block:

private void SpinBeforeBlocking()
{
    var wait = new SpinWait();
    while (!_condition)
    {
        // Utilizzando questa property posso sapere se sono arrivato al numero massimo di spin disponibili e il prossimo spin farà uno yielding
        if (wait.NextSpinWillYield)
        {
            /* block! */
        }
        else
        {
            wait.SpinOnce();
        }
    }
}

E’ estremamente interessante e ben commentato il codice ufficiale di Microsoft, quindi consiglio di darci una letta.

Indice

Share
Ultimi articoli

Codice Pragmatico

Contatti

Per informazioni, dubbi o consulenze non esitate a contattarmi.

Lascia un messaggio

Ricevi le ultime news

Iscrivi alla newsletter

Solo articoli interessanti, promesso ;)