Dal .NET 4 Γ¨ stato introdotto un nuovo tipo di ManualResetEvent chiamato ManualResetEventSlim
che permette di avere delle performance migliori qualora il tempo di blocco atteso sia molto breve.
Questo miglioramento viene effettuato effettuando dello spinning per un determinato numero di operazioni prima di effettuare il context switch e passare al blocking.
Permette, inoltre, di cancellare un Wait
utilizzando un CancellationToken
, cosa impossibile con i classici ManualResetEvent
.
La classe permette o a costruttore o mediante la property SpinCount
di impostare il numero di spin da effettuare prima di effettuare un vero block.
Utilizzo
Lβutilizzo di un ManualResetEventSlim
Γ¨ estremamente simile al suo simile ManualResetEvent
, le differenze sono le seguenti:
- Il metodo
WaitOne()
deiManualResetEvent
Γ¨ stato rinominato inWait()
che ha in ingresso, oltre ad un timeout in ms, anche unTimeSpan
o unCancellation[[Token]]
. - Presenta una property get-only
IsSet
che permette di sapere se Γ¨ stato o meno settato (di fatto analoga aWaitOne(0)
)
Chi preferire?
Dal libro C# 9.0 in a Nutshell leggo che ManualResetEventSlim
puΓ² essere fino a 50 volte piΓΉ veloce in scenari dove lβattesa Γ¨ minima in quanto non vi Γ¨ alcun passaggio al sistema operativo.
Per capire quanto deve essere minima questa attesa ho creato il seguente benchmark:
[ShortRunJob]
public class ManualResetEventTest
{
[Params(0, 1, 10)]
public int MillisecondsSleep;
[Benchmark]
public void ManualResetEventSlim()
{
using var mres = new ManualResetEventSlim(false);
var t = Task.Run(() =>
{
mres.Wait();
});
Thread.Sleep(MillisecondsSleep);
mres.Set();
t.Wait();
}
[Benchmark]
public void ManualResetEvent()
{
using var mres = new ManualResetEvent(false);
var t = Task.Run(() =>
{
mres.WaitOne();
});
Thread.Sleep(MillisecondsSleep);
mres.Set();
t.Wait();
}
}
Che porta ai seguenti risultati:
Come si puΓ² notare quando il tempo di attesa Γ¨ maggiore o uguale a 1ms i due metodi si equivalgono. La differenza sostanziale si ha quando ho un tempo di sleep di 0ms (quindi un solo context switch senza ulteriori attese) dove ho un miglioramento di performance di 6x.
Per rimuovere lβoverhead della creazione dei task ho pensato a questo ulteriore benchmark:
[Benchmark]
public void ManualResetEventSlimSetWait()
{
using var mres = new ManualResetEventSlim(false);
mres.Set();
mres.Wait();
}
[Benchmark]
public void ManualResetEventSetWait()
{
using var mres = new ManualResetEvent(false);
mres.Set();
mres.WaitOne();
}
In questo esempio al momento del wait il ManualResetEvent
Γ¨ giΓ stato impostato a true, di fatto non ho quindi alcuna attesa.
I risultati sono i seguenti:
Il ManualResetEventSlim
, qualora il semaforo sia giΓ stato settato, Γ¨ decisamente piΓΉ veloce; stiamo parlando di circa 2 ordini di grandezza.
Conclusioni
Nella stragrande maggioranza dei casi utilizzare un ManualResetEvent
Γ¨ piΓΉ che sufficiente, la differenza di performance Γ¨ trascurabile.
Ha senso utilizzare il ManualResetEventSlim
in caso eventi dove spesso lβattesa Γ¨ nulla (Γ¨ giΓ stato effettuato un Set()
) oppure inferiore a 1ms.