No, quello che hai non è sicuro. Il controllo per vedere se _count >= MAXIMUM
potrebbe correre con la chiamata a Interlocked.Increment
da un altro thread. Questo è in realtà davvero difficile da risolvere utilizzando tecniche di blocco basso. Per fare in modo che funzioni correttamente, è necessario eseguire una serie di operazioni diverse in modo atomico senza utilizzare un lucchetto. Questa è la parte difficile. La serie di operazioni in questione sono:
- Leggi
_count
- prova
_count >= MAXIMUM
- prendere una decisione basata su quanto sopra.
- Incremento
_count
in base alla decisione presa.
Se non fai apparire tutti e 4 questi passaggi atomici, allora ci sarà una condizione di gara. Il modello standard per eseguire un'operazione complessa senza prendere un blocco è il seguente.
public static T InterlockedOperation<T>(ref T location)
{
T initial, computed;
do
{
initial = location;
computed = op(initial); // where op() represents the operation
}
while (Interlocked.CompareExchange(ref location, computed, initial) != initial);
return computed;
}
Avviso che cosa sta succedendo. L'operazione viene ripetutamente eseguita fino a quando l'operazione ICX determina che il valore iniziale non è cambiato tra il momento in cui è stato letto per la prima volta e il momento in cui è stato effettuato il tentativo di cambiarlo. Questo è il modello standard e la magia si verifica tutto a causa della chiamata CompareExchange
(ICX). Si noti, tuttavia, che ciò non tiene conto dello ABA problem.
Che cosa si potrebbe fare:
Quindi, prendendo il modello di cui sopra e inserirla nel codice si tradurrebbe in questo.
public void CheckForWork()
{
int initial, computed;
do
{
initial = _count;
computed = initial < MAXIMUM ? initial + 1 : initial;
}
while (Interlocked.CompareExchange(ref _count, computed, initial) != initial);
if (replacement > initial)
{
Task.Run(() => Work());
}
}
Personalmente, vorrei punt sulla strategia a basso serratura del tutto. Ci sono diversi problemi con quello che ho presentato sopra.
- Questo potrebbe effettivamente essere più lento di un blocco rigido. Le ragioni sono difficili da spiegare e al di fuori della portata della mia risposta.
- Qualsiasi deviazione da quanto sopra può causare il fallimento del codice. Sì, è davvero così fragile.
- È difficile da capire. Voglio dire guardalo. È brutto.
Quello che dovrebbe fare:
Andando con il percorso di blocco duro il codice potrebbe essere simile.
private object _lock = new object();
private int _count;
public void CheckForWork()
{
lock (_lock)
{
if (_count >= MAXIMUM) return;
_count++;
}
Task.Run(() => Work());
}
public void CompletedWorkHandler()
{
lock (_lock)
{
_count--;
}
}
Si noti che questo è molto più semplice e considerevolmente meno incline agli errori. Si può effettivamente scoprire che questo approccio (hard lock) è in realtà più veloce di quello che ho mostrato sopra (low lock). Di nuovo, la ragione è complicata e ci sono tecniche che possono essere utilizzate per accelerare le cose, ma al di fuori dello scopo di questa risposta.
Il problema ABA non è realmente un problema in questo caso perché la logica non dipende _count
rimanendo invariata. Importa solo che il suo valore è lo stesso in due punti nel tempo, indipendentemente da quello che è successo nel mezzo. In altre parole, il problema può essere ridotto a uno in cui è sembrava come il valore non è cambiato anche se in realtà potrebbe avere.
Letteralmente "BWAHAHAHAHAH!" al titolo qui. –
Voglio solo sottolineare, questo è un modo piuttosto brutto per implementare ciò che è essenzialmente una Task Factory con un grado massimo di parallelismo. Ci sono tutta una serie di problemi, sia dal punto di vista del design (immagino che CheckForWork() venga chiamato su un timer e nel codice "...", ma questo è un problema. Implementazione del pool di thread che sta vanificando molto di ciò che si sta tentando di fare) e l'implementazione (Il problema più ovvio: se Work() genera un'eccezione o qualcuno si dimentica di chiamare CompletedWorkHandler() si affama la coda di lavoro) . – Chuu