realtà è thread-safe (semplicemente come una questione di un particolare implementazione su Count
), ma:
frammenti thread-safe di codice, non un thread-safe applicazione make. È possibile combinare diverse operazioni thread-safe in operazioni non thread-safe. In effetti, molti codici non thread-safe possono essere scomposti in pezzi più piccoli, che sono tutti thread-safe da soli.
Non è thread-safe per il motivo che speravi, il che significa che estenderlo ulteriormente non sarebbe thread-safe.
Questo codice sarebbe thread-safe:
public void CallToMethodInOtherClass(List<int> list)
{
//note we've no locks!
int i = list.Count;
//do something with i but don't touch list again.
}
chiamata con qualsiasi elenco, e si metterà a dare i
un valore in base allo stato di tale elenco, indipendentemente da ciò che altri thread sono fino a. Non corrompe list
. Non darà a i
un valore non valido.
Così, mentre questo codice è thread-safe:
public void CallToMethodInOtherClass(List<int> list)
{
Console.WriteLine(list[93]); // obviously only works if there's at least 94 items
// but that's nothing to do with thread-safety
}
Questo codice non sarebbe thread-safe:
public void CallToMethodInOtherClass(List<int> list)
{
lock(locker)//same as in the question, different locker to that used elsewhere.
{
int i = list.Count;
if(i > 93)
Console.WriteLine(list[93]);
}
}
Prima di proseguire, i due bit sono descritti come filettatura la sicurezza non è promessa dalle specifiche per l'elenco. La codifica conservativa presuppone che non siano thread-safe anziché dipendere dai dettagli dell'implementazione, ma dipenderò dai dettagli di implementazione perché influisce sulla domanda su come utilizzare i lock in un modo importante:
Perché c'è codice operativo su list
che non sta acquisendo il blocco su locker
per prima cosa, tale codice non viene impedito di essere eseguito contemporaneamente a CallToMethodInOtherClass
. Ora, mentre list.Count
è thread-safe e list[93]
è sicuro, * la combinazione dei due in cui dipendiamo dal primo per garantire che il secondo non sia thread-safe. Poiché il codice al di fuori del blocco può influire su list
, è possibile che il codice chiami Remove
o Clear
tra Count
assicurandoci che list[93]
funzionerebbe e list[93]
chiamato.
Ora, se sappiamo che list
viene aggiunto solo a, va bene, anche se un ridimensionamento sta accadendo contemporaneamente finiremo con il valore di list[93]
in entrambi i casi. Se qualcosa sta scrivendo su list[93]
ed è un tipo che .NET scrive a livello atomico (e lo int
è uno di questi tipi), finiremo con quello vecchio o quello nuovo, proprio come se avessimo bloccato correttamente noi 'Ottenere il vecchio o il nuovo a seconda di quale thread andare il blocco prima.Anche in questo caso, questo è un dettaglio di implementazione che non è una promessa specifica, lo sto affermando solo per sottolineare come la sicurezza del thread fornita abbia ancora come risultato un codice non thread-safe.
Spostare questo verso il codice reale. Non dovremmo dare per scontato che list.Count
e list[93]
siano infallibili perché non ci avevano promesso che sarebbero stati cambiati, ma anche se avessimo avuto quella promessa, quelle due promesse non si sarebbero aggiunte alla promessa che sarebbero state thread-safe insieme.
L'importante è utilizzare lo stesso blocco per proteggere i blocchi di codice che possono interferire l'uno con l'altro. Quindi, si consideri la variante di sotto che è garantito essere threadsafe:
public class ThreadSafeList
{
private readonly object locker = new object();
private List<int> myList = new List<int>();
public void Add(int item)
{
lock(locker)
myList.Add(item);
}
public void Clear()
{
lock(locker)
myList.Clear();
}
public int Count
{
lock(locker)
return myList.Count;
}
public int Item(int index)
{
lock(locker)
return myList[index];
}
}
Questa classe è garantito essere threadsafe in ogni sua attività. Senza dipendere da alcun dettaglio di implementazione, qui non esiste alcun metodo che possa corrompere lo stato o fornire risultati errati a causa di ciò che un altro thread sta facendo con la stessa istanza. Il seguente codice ancora non funziona però:
// (l is a ThreadSafeList visible to multiple threads.
if(l.Count > 0)
Console.WriteLine(l[0]);
Abbiamo garantito il filo di sicurezza di ogni chiamata al 100%, ma non abbiamo garantito la combinazione, e noi non ci riesce garanzia la combinazione.
Ci sono due cose che possiamo fare. Possiamo aggiungere un metodo per la combinazione. Qualcosa come la seguente sarebbe comune per molte classi specificamente progettati per uso multi-threaded:
public bool TryGetItem(int index, out int value)
{
lock(locker)
{
if(l.Count > index)
{
value = l[index];
return true;
}
value = 0;
return false;
}
}
Questo rende il test conteggio e la parte elemento recupero di una singola operazione che è garantito per essere thread-safe.
Alternativamente, e più spesso ciò che dobbiamo fare, abbiamo capita la serratura nel luogo in cui sono raggruppate le operazioni:
lock(lockerOnL)//used by every other piece of code operating on l
if(l.Count > 0)
Console.WriteLine(l[0]);
Naturalmente, questo rende le serrature all'interno ThreadSafeList
ridondante e solo uno spreco di sforzo, spazio e tempo. Questo è il motivo principale per cui la maggior parte delle classi non fornisce thread-safety sui membri delle istanze - dal momento che non è possibile proteggere in modo significativo gruppi di chiamate sui membri all'interno della classe, è una perdita di tempo provare a meno che le promesse sulla sicurezza dei thread sono molto ben specificati e utili da soli.
Per tornare al codice nella tua domanda:
Il blocco in CallToMethodInOtherClass
dovrebbe essere rimosso a meno che OtherClass
ha la sua ragione di bloccaggio internamente. Non può fare una promessa significativa che non verrà combinato in modo non sicuro e aggiungere più blocchi a un programma aumenta la complessità dell'analisi per assicurarsi che non ci siano deadlock.
La chiamata a CallToMethodInOtherClass
deve essere protetto dalla stessa serratura come altre operazioni in quella classe:
public void MethodeB()
{
lock(locker)
CallToMethodInOtherClass(myList);
}
Poi, finché CallToMethodInOtherClass
non memorizza myList
da qualche parte che può essere visto da altri thread in seguito, non importa che CallToMethodInOtherClass
non sia thread-safe perché l'unico codice che può accedere a myList
porta la propria garanzia di non chiamarlo in concomitanza con altre operazioni su myList
.
Le due cose importanti sono:
Quando qualcosa è descritto come "thread-safe", so solo ciò che è promettente che, in quanto vi sono diversi tipi di promessa che cadono sotto "thread-safe "e da solo significa solo" non metterò questo oggetto in uno stato senza senso ", che sebbene sia un elemento importante, non è molto da solo.
Lock gruppi delle operazioni, con lo stesso blocco per ogni gruppo che ti influenzano gli stessi dati, e la guardia l'accesso agli oggetti in modo che non ci può eventualmente essere un altro thread non giocare a palla con questo.
* Questa è una definizione molto limitata di thread-safe. Chiamando list[93]
su un List<T>
dove T
è un tipo che verrà scritto e letto in modo atomico e non sappiamo se in realtà ha almeno 94 elementi ugualmente sicuri indipendentemente dal fatto che ci siano altri thread operativi su di esso. Ovviamente, il fatto che sia in grado di lanciare ArgumentOutOfRangeException
in entrambi i casi non è ciò che la maggior parte delle persone considererebbe "sicuro", ma la garanzia che abbiamo con più thread rimane la stessa di una. È che otteniamo una garanzia più forte controllando Count
in un singolo thread ma non in una situazione multi-thread che mi porta a descriverlo come non thread-safe; mentre quella combo continua a non corrompere lo stato, può portare a un'eccezione che ci saremmo assicurati che non potesse accadere.
Il tuo bagno ha due porte, ognuna con un lucchetto. La tua domanda è "supponiamo che io blocchi solo il primo lucchetto quando sono sotto la doccia e il mio amico Bob blocca solo il secondo lucchetto quando è sotto la doccia, potremmo mai finire entrambi nella doccia contemporaneamente?" Ovviamente sì! Se tu e Bob volete evitare di fare la doccia insieme, dovete accettare entrambi di usare * lo stesso lucchetto *. Non è possibile rendere l'accesso a un oggetto sicuro come questo. –