come sapere cosa NON è thread-safe in ruby?


93

a partire da Rails 4 , tutto dovrebbe essere eseguito in ambiente thread per impostazione predefinita. Ciò significa che tutto il codice che scriviamo AND TUTTE le gemme che usiamo devono esserethreadsafe

quindi, ho alcune domande su questo:

  1. cosa NON è thread-safe in ruby ​​/ rails? Vs Cos'è thread-safe in ruby ​​/ rails?
  2. C'è un elenco di gemme che è noto per essere threadsafe o viceversa?
  3. c'è un elenco di modelli comuni di codice che NON sono un esempio sicuro per i thread @result ||= some_method?
  4. Le strutture dati in ruby ​​lang core come Hashecc threadsafe?
  5. Sulla risonanza magnetica, dove c'è un GVL/GIL che significa che solo 1 filo rubino può essere eseguito alla volta tranne IO, il cambiamento di sicurezza per i thread ci ha effetto?

2
Sei sicuro che tutto il codice e tutte le gemme DEVONO essere thread-safe? Quello che dicono le note di rilascio è che Rails stesso sarà sicuro per i thread, non che tutto il resto usato con esso DEVE essere
enthrops

I test multi-threaded sarebbero il peggior rischio possibile per la sicurezza dei thread. Quando devi modificare il valore di una variabile di ambiente attorno al tuo caso di test, non sei immediatamente sicuro per i thread. Come lo aggirereste? E sì, tutte le gemme devono essere sicure.
Lukas Oberhuber

Risposte:


110

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 += 1non è 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 stuffaccesso. 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_stufffunziona bene la prima volta che viene utilizzato, ma restituisce qualcos'altro la seconda volta. Perché? Il load_thingsmetodo pensa di possedere le opzioni hash passate, e lo fa color = options.delete(:color). Ora la STANDARD_OPTIONScostante 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.


Bella risposta. Considerando che una tipica app rails è multi-processo (come hai descritto, molti utenti diversi accedono alla stessa app), mi chiedo quale sia il rischio marginale dei thread per il modello di concorrenza ... In altre parole, quanto più "pericoloso" deve essere eseguito in modalità thread se hai già a che fare con una certa concorrenza tramite processi?
gingerlime

2
@Theo Grazie mille. Quella roba costante è una grande bomba. Non è nemmeno sicuro per il processo. Se la costante viene modificata in una richiesta, le richieste successive vedranno la costante modificata anche in un singolo thread. Le costanti di Ruby sono strani
rubish

5
Fare STANDARD_OPTIONS = {...}.freezeper aumentare su mutazioni superficiali
glebm

Davvero un'ottima risposta
Cheyne

3
"Se scrivi codice come @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }[...], il valore della variabile condivisa in seguito non è deterministico." - Sai se questo differisce tra le versioni di Ruby? Ad esempio, l'esecuzione del codice su 1.8 fornisce valori diversi di @n, ma su 1.9 e @n
versioni

10

Oltre alla risposta di Theo, aggiungerei un paio di aree problematiche da cercare in Rails in particolare, se stai passando a config.threadsafe!

  • Variabili di classe :

    @@i_exist_across_threads

  • ENV :

    ENV['DONT_CHANGE_ME']

  • Discussioni :

    Thread.start


9

a partire da Rails 4, tutto dovrebbe essere eseguito in ambiente thread per impostazione predefinita

Questo non è corretto al 100%. Rails thread-safe è solo attivo per impostazione predefinita. Se distribuisci su un server di app multi-processo come Passenger (community) o Unicorn, non ci sarà alcuna differenza. Questa modifica riguarda solo te, se distribuisci su un ambiente multi-thread come Puma o Passenger Enterprise> 4.0

In passato, se volevi distribuire su un server di app multi-thread , dovevi attivare config.threadsafe , che ora è l'impostazione predefinita, perché tutto ciò che faceva non aveva effetti o si applicava anche a un'app Rails in esecuzione in un unico processo ( Prooflink ).

Ma se vuoi tutti i vantaggi dello streaming di Rails 4 e altre cose in tempo reale della distribuzione multi-thread, forse troverai questo articolo interessante. Come @Theo sad, per un'app Rails, in realtà devi solo omettere lo stato statico mutante durante una richiesta. Sebbene sia una pratica semplice da seguire, sfortunatamente non puoi esserne sicuro per ogni gemma che trovi. Per quanto ricordo Charles Oliver Nutter del progetto JRuby aveva alcuni suggerimenti in merito in questo podcast.

E se vuoi scrivere una programmazione Ruby simultanea pura, dove avresti bisogno di alcune strutture di dati a cui si accede da più di un thread, forse troverai utile la gemma thread_safe .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.