Nessuna delle strutture di dati principali è thread-safe. L'unico che conosco che viene fornito con Ruby è l'implementazione della coda nella libreria standard ( require 'thread'; q = Queue.new
).
Il GIL di MRI non ci salva dai problemi di sicurezza dei thread. Fa solo in modo che due thread non possano eseguire il codice Ruby allo stesso tempo , cioè su due CPU diverse allo stesso tempo. I thread possono ancora essere sospesi e ripresi in qualsiasi punto del codice. Se scrivi codice come @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }
ad esempio la modifica di una variabile condivisa da più thread, il valore della variabile condivisa in seguito non è deterministico. Il GIL è più o meno una simulazione di un sistema single core, non cambia le questioni fondamentali della scrittura di programmi concorrenti corretti.
Anche se la risonanza magnetica fosse stata a thread singolo come Node.js, dovresti comunque pensare alla concorrenza. L'esempio con la variabile incrementata funzionerebbe bene, ma puoi comunque ottenere condizioni di gara in cui le cose avvengono in ordine non deterministico e un callback ostruisce il risultato di un altro. I sistemi asincroni a thread singolo sono più facili da ragionare, ma non sono esenti da problemi di concorrenza. Pensa solo a un'applicazione con più utenti: se due utenti premono la modifica su un post di Stack Overflow più o meno contemporaneamente, dedica un po leggi quello stesso post?
In Ruby, come nella maggior parte degli altri runtime simultanei, tutto ciò che è più di un'operazione non è thread-safe. @n += 1
non è thread-safe, perché si tratta di più operazioni. @n = 1
è thread-safe perchéèun'operazione (ci sono molte operazioni sotto il cofano, e probabilmente mi metterei nei guai se provassi a descrivere il motivo per cui è "thread safe" in dettaglio, ma alla fine non otterrai risultati incoerenti dagli incarichi ). @n ||= 1
, non lo è e nessun'altra operazione abbreviata + assegnazione lo è. Un errore che ho commesso molte volte è stato scrivere return unless @started; @started = true
, il che non è affatto thread safe.
Non conosco alcun elenco autorevole di istruzioni thread-safe e non-thread-safe per Ruby, ma esiste una semplice regola pratica: se un'espressione esegue solo un'operazione (priva di effetti collaterali), probabilmente è thread-safe. Ad esempio: a + b
è ok, a = b
è anche ok, ed a.foo(b)
è ok, se il metodo foo
è privo di effetti collaterali (poiché praticamente qualsiasi cosa in Ruby è una chiamata al metodo, anche l'assegnazione in molti casi, questo vale anche per gli altri esempi). Effetti collaterali in questo contesto significano cose che cambiano stato. nondef foo(x); @x = x; end
è privo di effetti collaterali.
Una delle cose più difficili della scrittura di codice thread-safe in Ruby è che tutte le strutture di dati principali, inclusi array, hash e stringhe, sono mutabili. È molto facile far trapelare accidentalmente un pezzo del tuo stato, e quando quel pezzo è mutabile le cose possono diventare davvero incasinate. Considera il codice seguente:
class Thing
attr_reader :stuff
def initialize(initial_stuff)
@stuff = initial_stuff
@state_lock = Mutex.new
end
def add(item)
@state_lock.synchronize do
@stuff << item
end
end
end
Un'istanza di questa classe può essere condivisa tra i thread e possono tranquillamente aggiungere cose ad essa, ma c'è un bug di concorrenza (non è l'unico): lo stato interno dell'oggetto trapela attraverso la funzione di stuff
accesso. Oltre ad essere problematico dal punto di vista dell'incapsulamento, apre anche una serie di worm di concorrenza. Forse qualcuno prende quell'array e lo passa da qualche altra parte, e quel codice a sua volta pensa di possedere quell'array e può fare quello che vuole con esso.
Un altro classico esempio di Ruby è questo:
STANDARD_OPTIONS = {:color => 'red', :count => 10}
def find_stuff
@some_service.load_things('stuff', STANDARD_OPTIONS)
end
find_stuff
funziona bene la prima volta che viene utilizzato, ma restituisce qualcos'altro la seconda volta. Perché? Il load_things
metodo pensa di possedere le opzioni hash passate, e lo fa color = options.delete(:color)
. Ora la STANDARD_OPTIONS
costante non ha più lo stesso valore. Le costanti sono costanti solo in ciò a cui fanno riferimento, non garantiscono la costanza delle strutture dati a cui si riferiscono. Pensa solo a cosa accadrebbe se questo codice venisse eseguito contemporaneamente.
Se eviti lo stato mutabile condiviso (ad esempio variabili di istanza in oggetti a cui accedono più thread, strutture dati come hash e array a cui accedono più thread) la sicurezza del thread non è così difficile. Cerca di ridurre al minimo le parti della tua applicazione a cui si accede contemporaneamente e concentrare i tuoi sforzi su di esse. IIRC, in un'applicazione Rails, viene creato un nuovo oggetto controller per ogni richiesta, quindi verrà utilizzato solo da un singolo thread, e lo stesso vale per qualsiasi oggetto del modello creato da quel controller. Tuttavia, Rails incoraggia anche l'uso di variabili globali (User.find(...)
usa la variabile globaleUser
, potresti considerarla solo una classe, ed è una classe, ma è anche uno spazio dei nomi per le variabili globali), alcune di queste sono sicure perché sono di sola lettura, ma a volte salvi le cose in queste variabili globali perché è conveniente. Fai molta attenzione quando usi qualcosa che è accessibile a livello globale.
È stato possibile eseguire Rails in ambienti threaded da un po 'di tempo ormai, quindi senza essere un esperto di Rails mi spingerei comunque a dire che non devi preoccuparti della sicurezza dei thread quando si tratta di Rails stesso. Puoi ancora creare applicazioni Rails che non sono thread-safe facendo alcune delle cose che ho menzionato sopra. Quando arriva altre gemme presumono che non siano thread-safe a meno che non dicano che lo sono, e se dicono che presumono che non lo siano, e guarda attraverso il loro codice (ma solo perché vedi che vanno cose come@n ||= 1
non significa che non siano thread-safe, è una cosa perfettamente legittima da fare nel giusto contesto: dovresti invece cercare cose come lo stato mutabile nelle variabili globali, come gestisce gli oggetti mutabili passati ai suoi metodi e soprattutto come gestisce gli hash delle opzioni).
Infine, essere thread unsafe è una proprietà transitiva. Tutto ciò che utilizza qualcosa che non è thread-safe di per sé non è thread-safe.