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 += 1
nã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 + b
está ok, a = b
também está ok, e a.foo(b)
está ok, se o método foo
for 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 stuff
acessador. 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_stuff
funciona bem na primeira vez que é usado, mas retorna outra coisa na segunda vez. Por quê? O load_things
método acontece a pensar que é dono do hash de opções passado para ele, e faz color = options.delete(:color)
. Agora a STANDARD_OPTIONS
constante 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.