como saber o que NÃO é thread-safe no ruby?

93

a partir do Rails 4 , tudo teria que rodar em um ambiente threaded por padrão. O que isso significa é que todo o código que escrevemos E TODAS as joias que usamos devem serthreadsafe

então, eu tenho algumas perguntas sobre isso:

  1. o que NÃO é thread-safe em ruby ​​/ rails? Vs O que é thread-safe em ruby ​​/ rails?
  2. Existe uma lista de jóias que é conhecido por ser threadsafe ou vice-versa?
  3. existe uma lista de padrões comuns de código que NÃO são exemplos threadsafe @result ||= some_method?
  4. As estruturas de dados no ruby ​​lang core, como Hashetc, são threadsafe?
  5. Na ressonância magnética, onde há um GVL/ oGIL que significa que apenas 1 thread ruby ​​pode ser executado por vez, exceto IO, a mudança de threadsafe nos afeta?
CuriousMind
fonte
2
Tem certeza de que todo o código e todas as gemas TÊM que ser threadsafe? O que as notas de lançamento dizem é que o próprio Rails será threadsafe, não que tudo o mais usado com ele TENHA que ser
entra em
Testes multithread seriam o pior risco threadsafe possível. Quando você tem que alterar o valor de uma variável de ambiente em torno de seu caso de teste, instantaneamente não é thread-safe. Como você resolveria isso? E sim, todas as joias devem ser threadsafe.
Lukas Oberhuber

Respostas:

110

Nenhuma das estruturas de dados principais é thread-safe. O único que conheço que vem com Ruby é a implementação de fila na biblioteca padrão ( require 'thread'; q = Queue.new).

O GIL da MRI não nos salva de problemas de segurança de thread. Ele apenas garante que dois threads não possam executar o código Ruby ao mesmo tempo , ou seja, em duas CPUs diferentes ao mesmo tempo. Threads ainda podem ser pausados ​​e retomados em qualquer ponto do código. Se você escrever código como, @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }por exemplo, alterar uma variável compartilhada de vários threads, o valor da variável compartilhada posteriormente não é determinístico. O GIL é mais ou menos uma simulação de um sistema de núcleo único, ele não muda as questões fundamentais de escrever programas concorrentes corretos.

Mesmo que a MRI fosse de thread único como o Node.js, você ainda teria que pensar sobre a simultaneidade. O exemplo com a variável incrementada funcionaria bem, mas você ainda pode obter condições de corrida em que as coisas acontecem em ordem não determinística e um retorno de chamada supera o resultado de outro. Os sistemas assíncronos de encadeamento único são mais fáceis de raciocinar, mas não estão livres de problemas de simultaneidade. Pense em um aplicativo com vários usuários: se dois usuários clicarem em editar em uma postagem do Stack Overflow mais ou menos ao mesmo tempo, passe algum tempo editando a postagem e clique em Salvar, cujas alterações serão vistas por um terceiro usuário mais tarde, quando eles leu essa mesma postagem?

Em Ruby, como na maioria dos outros tempos de execução simultâneos, qualquer coisa que seja mais de uma operação não é thread-safe. @n += 1não é seguro para thread, porque é várias operações. @n = 1é thread-safe porque é uma operação (são muitas operações por baixo do capô, e eu provavelmente teria problemas se tentasse descrever por que é "thread-safe" em detalhes, mas no final você não obterá resultados inconsistentes de atribuições ) @n ||= 1, não é e nenhuma outra operação abreviada + atribuição é. Um erro que cometi muitas vezes é escrever return unless @started; @started = true, o que não é seguro para threads.

Não conheço nenhuma lista autorizada de instruções thread-safe e non-thread safe para Ruby, mas existe uma regra simples: se uma expressão faz apenas uma operação (sem efeitos colaterais), provavelmente é thread-safe. Por exemplo: a + bestá ok, a = btambém está ok, e a.foo(b)está ok, se o método foofor livre de efeitos colaterais (uma vez que quase tudo em Ruby é uma chamada de método, mesmo atribuição em muitos casos, isso vale para os outros exemplos também). Os efeitos colaterais neste contexto significam coisas que mudam de estado. nãodef foo(x); @x = x; end é livre de efeitos colaterais.

Uma das coisas mais difíceis sobre escrever código thread-safe em Ruby é que todas as estruturas de dados centrais, incluindo array, hash e string, são mutáveis. É muito fácil vazar acidentalmente uma parte do seu estado e, quando essa parte é mutável, as coisas podem ficar realmente complicadas. Considere o seguinte código:

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

Uma instância dessa classe pode ser compartilhada entre threads e eles podem adicionar coisas a ela com segurança, mas há um bug de simultaneidade (não é o único): o estado interno do objeto vaza através do stuffacessador. Além de ser problemático do ponto de vista do encapsulamento, ele também abre uma lata de worms de simultaneidade. Talvez alguém pegue aquele array e o passe para outro lugar, e esse código, por sua vez, pensa que agora possui esse array e pode fazer o que quiser com ele.

Outro exemplo clássico de Ruby é este:

STANDARD_OPTIONS = {:color => 'red', :count => 10}

def find_stuff
  @some_service.load_things('stuff', STANDARD_OPTIONS)
end

find_stufffunciona bem na primeira vez que é usado, mas retorna outra coisa na segunda vez. Por quê? O load_thingsmétodo acontece a pensar que é dono do hash de opções passado para ele, e faz color = options.delete(:color). Agora a STANDARD_OPTIONSconstante não tem mais o mesmo valor. As constantes são constantes apenas naquilo que referenciam, não garantem a constância das estruturas de dados a que se referem. Pense no que aconteceria se esse código fosse executado simultaneamente.

Se você evitar o estado mutável compartilhado (por exemplo, variáveis ​​de instância em objetos acessados ​​por vários threads, estruturas de dados como hashes e arrays acessados ​​por vários threads) a segurança de thread não é tão difícil. Tente minimizar as partes de seu aplicativo que são acessadas simultaneamente e concentre seus esforços nisso. IIRC, em uma aplicação Rails, um novo objeto controlador é criado para cada requisição, então ele só será usado por uma única thread, e o mesmo vale para qualquer objeto modelo que você criar a partir daquele controlador. No entanto, Rails também incentiva o uso de variáveis ​​globais ( User.find(...)usa a variável globalUser, você pode pensar nisso apenas como uma classe, e é uma classe, mas também é um namespace para variáveis ​​globais), algumas delas são seguras porque são somente leitura, mas às vezes você salva coisas nessas variáveis ​​globais porque é conveniente. Tenha muito cuidado ao usar qualquer coisa que seja globalmente acessível.

É possível rodar Rails em ambientes threaded há um bom tempo, então sem ser um especialista em Rails eu ainda iria mais longe e diria que você não precisa se preocupar com segurança de thread quando se trata do Rails em si. Você ainda pode criar aplicações Rails que não sejam thread-safe fazendo algumas das coisas que mencionei acima. Quando se trata de outras joias presumem que não são thread-safe a menos que digam que são, e se dizem que são, presumem que não são, e olhe através de seu código (mas só porque você vê que eles fazem coisas como@n ||= 1 não significa que eles não sejam thread-safe, isso é uma coisa perfeitamente legítima de se fazer no contexto certo - você deve, em vez disso, procurar coisas como estado mutável em variáveis ​​globais, como ele lida com objetos mutáveis ​​passados ​​para seus métodos, e especialmente como ele lida com hashes de opções).

Finalmente, ser thread não seguro é uma propriedade transitiva. Qualquer coisa que use algo que não seja seguro para thread não é seguro para threads.

Theo
fonte
Ótima resposta. Considerando que um aplicativo Rails típico é multiprocesso (como você descreveu, muitos usuários diferentes acessando o mesmo aplicativo), estou me perguntando qual é o risco marginal de threads para o modelo de simultaneidade ... Em outras palavras, quanto mais "perigoso" é para ser executado no modo encadeado se você já estiver lidando com alguma simultaneidade por meio de processos?
gingerlime
2
@Theo Muito obrigado. Essa coisa constante é uma grande bomba. Nem mesmo é seguro para o processo. Se a constante for alterada em uma solicitação, isso fará com que as solicitações posteriores vejam a constante alterada, mesmo em um único encadeamento. As constantes Ruby são estranhas
rubis,
5
Faça STANDARD_OPTIONS = {...}.freezepara aumentar em mutações superficiais
glebm
Resposta realmente ótima
Cheyne
3
"Se você escrever código como @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }[...], o valor da variável compartilhada posteriormente não é determinístico." - Você sabe se isso difere entre as versões do Ruby? Por exemplo, executar seu código em 1.8 fornece valores diferentes de @n, mas em 1.9 e posteriores parece dar consistentemente @nigual a 300.
user200783
10

Além da resposta de Theo, eu adicionaria algumas áreas problemáticas a serem observadas em Rails especificamente, se você estiver mudando para config.threadsafe!

  • Variáveis ​​de classe :

    @@i_exist_across_threads

  • ENV :

    ENV['DONT_CHANGE_ME']

  • Tópicos :

    Thread.start

crizCraig
fonte
9

a partir do Rails 4, tudo teria que ser executado em ambiente encadeado por padrão

Isso não é 100% correto. O Rails thread-safe está ativado por padrão. Se você implantar em um servidor de aplicativos multiprocessos como Passenger (comunidade) ou Unicorn, não haverá nenhuma diferença. Esta mudança só diz respeito a você, se você implantar em um ambiente multi-thread como Puma ou Passenger Enterprise> 4.0

No passado, se você quisesse implantar em um servidor de aplicativo multi-threaded, você tinha que ativar config.threadsafe , que é o padrão agora, porque tudo o que ele fazia não tinha efeitos ou também se aplicava a um aplicativo Rails rodando em um único processo ( Prooflink ).

Mas se você quiser todos os benefícios de streaming do Rails 4 e outras coisas em tempo real da implantação multi-threaded, talvez você ache este artigo interessante. Como @Theo lamenta, para um aplicativo Rails, você na verdade apenas tem que omitir a mutação do estado estático durante uma solicitação. Embora seja uma prática simples de seguir, infelizmente você não pode ter certeza sobre isso para cada joia que encontrar. Pelo que me lembro, Charles Oliver Nutter do projeto JRuby tinha algumas dicas sobre isso neste podcast.

E se você quiser escrever uma programação Ruby simultânea pura, onde você precisaria de algumas estruturas de dados que são acessadas por mais de um thread, talvez você ache a gem thread_safe útil.

dre-hh
fonte