ruby multithreading concurrency thread-safety ruby-on-rails-4

cómo saber qué NO es seguro para subprocesos en ruby?



multithreading concurrency (3)

a partir de Rails 4, todo debería ejecutarse en un entorno con hebras por defecto

Esto no es 100% correcto. Threadsafe Rails está activado por defecto. Si todavía se implementa en un servidor de aplicaciones multiproceso como pasajero (comunidad) o unicornio, no habrá ninguna diferencia. Este cambio solo le concierne si se implementa en un entorno de subprocesos múltiples como Puma o Passenger Enterprise> 4.0

En el pasado, si quería implementar en un servidor de aplicaciones de subprocesos múltiples, tenía que activar config.threadsafe , que ahora es el predeterminado, porque todo lo que hizo no tuvo ningún efecto o también se aplicó a una aplicación de rieles que se ejecuta en un único proceso ( Prooflink ).

Pero si usted quiere todos los beneficios de streaming rieles 4 y otras cosas en tiempo real de la implementación multiproceso, entonces tal vez encuentre this artículo interesante. Como @Theo sad, para una aplicación de rieles, simplemente tienes que omitir el estado estático de mutación durante una solicitud. Si bien es una práctica sencilla de seguir, desafortunadamente no puede estar seguro de esto por cada gema que encuentre. Por lo que recuerdo, Charles Oliver Nutter del proyecto Jruby tuvo algunos consejos al respecto en this podcast.

Y si quieres escribir una programación simultánea de Ruby pura, donde necesitarías algunas estructuras de datos a las que se accede por más de un hilo, quizás encuentres thread_safe gema thread_safe

a partir de Rails 4 , todo debería ejecutarse en un entorno con hebras por defecto. Lo que esto significa es todo el código que escribimos Y TODAS las gemas que usamos son requeridas para ser threadsafe

entonces, tengo pocas preguntas sobre esto:

  1. ¿Qué NO es seguro para subprocesos en ruby ​​/ rails? Vs ¿Qué es thread-safe en ruby ​​/ rails?
  2. ¿Hay una lista de gemas que se sabe que son enhebrables o viceversa?
  3. ¿Hay una lista de patrones comunes de código que NO son ejemplos de threadsafe @result ||= some_method ?
  4. ¿Las estructuras de datos en Ruby Lang, como Hash etc. son seguras para el hilo?
  5. En MRI, donde hay un GVL/GIL que significa que solo se puede ejecutar 1 hilo de rubí a la vez, excepto IO , ¿nos afecta el cambio de hilo?

Además de la respuesta de Theo, agregaría un par de áreas problemáticas para buscar específicamente en Rails, ¡si cambias a config.threadsafe!

  • Variables de clase :

    @@i_exist_across_threads

  • ENV :

    ENV[''DONT_CHANGE_ME'']

  • Hilos :

    Thread.start


Ninguna de las estructuras de datos centrales es segura para subprocesos. El único que sé de que los barcos con Ruby es la implementación de cola en la biblioteca estándar ( require ''thread''; q = Queue.new ).

El GIL de MRI no nos salva de los problemas de seguridad de los hilos. Solo se asegura de que dos subprocesos no puedan ejecutar código Ruby al mismo tiempo , es decir, en dos CPU diferentes al mismo tiempo. Los hilos aún se pueden pausar y reanudar en cualquier punto de su código. Si escribe código como @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } } @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } } por ejemplo, mutando una variable compartida de múltiples hilos, el valor de la variable compartida después no es determinista. El GIL es más o menos una simulación de un sistema de núcleo único, no cambia los problemas fundamentales de escribir programas simultáneos correctos.

Incluso si la resonancia magnética hubiera sido de un solo hilo como Node.js, aún tendría que pensar en la concurrencia. El ejemplo con la variable incrementada funcionaría bien, pero aún puede obtener condiciones de carrera donde las cosas suceden en un orden no determinista y una devolución de llamada corta el resultado de otra. Los sistemas asíncronos de un solo hilo son más fáciles de razonar, pero no están exentos de problemas de concurrencia. Solo piense en una aplicación con varios usuarios: si dos usuarios presionan editar en una publicación de más o menos al mismo tiempo, dedique un tiempo a editarla y luego presione Guardar, cuyos cambios serán vistos por un tercer usuario más adelante cuando leer esa misma publicación?

En Ruby, como en la mayoría de los demás tiempos de ejecución simultáneos, cualquier cosa que sea más de una operación no es segura para subprocesos. @n += 1 no es seguro para subprocesos, porque se trata de operaciones múltiples. @n = 1 es seguro para subprocesos porque es una operación (hay muchas operaciones bajo el capó, y probablemente me metería en problemas si intentara describir por qué es "seguro para subprocesos" en detalle, pero al final no obtendrás resultados inconsistentes de las asignaciones). @n ||= 1 , no es y ninguna otra operación de taquigrafía + asignación es tampoco. Un error que he cometido muchas veces es escribir a return unless @started; @started = true return unless @started; @started = true , que no es seguro para subprocesos.

No conozco ninguna lista autorizada de sentencias de seguridad de subprocesos y no subprocesos para Ruby, pero hay una regla general: si una expresión solo realiza una operación (sin efectos secundarios) es probable que sea segura para subprocesos. Por ejemplo: a + b está bien, a = b también está bien, y a.foo(b) está bien, si el método foo es libre de efectos secundarios (ya que casi todo en Ruby es una llamada a método, incluso asignación en en muchos casos, esto también se aplica a los otros ejemplos). Los efectos secundarios en este contexto significan cosas que cambian de estado. def foo(x); @x = x; end def foo(x); @x = x; end no es libre de efectos secundarios.

Una de las cosas más difíciles de escribir código seguro para subprocesos en Ruby es que todas las estructuras de datos centrales, incluidos matriz, hash y cadena, son mutables. Es muy fácil filtrar accidentalmente un trozo de tu estado, y cuando esa pieza es mutable, las cosas se pueden estropear. Considera el siguiente 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

Una instancia de esta clase se puede compartir entre subprocesos y pueden agregarle cosas de manera segura, pero hay un error de concurrencia (no es el único): el estado interno del objeto se filtra a través del accesorio de stuff . Además de ser problemático desde la perspectiva de la encapsulación, también abre una lata de gusanos de concurrencia. Tal vez alguien tome esa matriz y la pase a otra parte, y ese código a su vez cree que ahora posee esa matriz y puede hacer lo que quiera con ella.

Otro ejemplo clásico de Ruby es este:

STANDARD_OPTIONS = {:color => ''red'', :count => 10} def find_stuff @some_service.load_things(''stuff'', STANDARD_OPTIONS) end

find_stuff funciona bien la primera vez que se usa, pero devuelve algo más la segunda vez. ¿Por qué? El método load_things pasa a pensar que posee el hash de opciones que se le pasó, y does color = options.delete(:color) . Ahora la constante STANDARD_OPTIONS ya no tiene el mismo valor. Las constantes son solo constantes en lo que hacen referencia, no garantizan la constancia de las estructuras de datos a las que se refieren. Solo piense qué pasaría si este código se ejecutara al mismo tiempo.

Si evita el estado mutable compartido (por ejemplo, variables de instancia en objetos a los que se accede por múltiples hilos, estructuras de datos como hashes y matrices a las que acceden múltiples hilos) la seguridad de las hebras no es tan difícil. Intente minimizar las partes de su aplicación a las que se accede simultáneamente y enfoque sus esfuerzos allí. IIRC, en una aplicación de Rails, se crea un nuevo objeto de controlador para cada solicitud, por lo que solo se utilizará con un único hilo, y lo mismo ocurre con cualquier objeto de modelo que cree desde ese controlador. Sin embargo, Rails también alienta el uso de variables globales ( User.find(...) usa la variable global User , puede pensar que es solo una clase, y es una clase, pero también es un espacio de nombres para variables globales ), algunos de estos son seguros porque son de solo lectura, pero a veces se guardan cosas en estas variables globales porque es conveniente. Tenga mucho cuidado cuando use cualquier cosa que sea accesible a nivel mundial.

Ha sido posible ejecutar Rails en entornos con hebras durante bastante tiempo, así que sin ser un experto en Rails, llegaría a decir que no tiene que preocuparse por la seguridad de los hilos cuando se trata de Rails. Aún puede crear aplicaciones de Rails que no sean seguras para subprocesos al hacer algunas de las cosas que mencioné anteriormente. Cuando se trata de otras gemas suponen que no son seguras para subprocesos a menos que digan que sí, y si dicen que suponen que no lo son, y @n ||= 1 su código (pero solo porque ven que van cosas como @n ||= 1 no significa que no sean seguros para subprocesos, eso es algo perfectamente legítimo en el contexto correcto; en su lugar, debería buscar cosas como el estado variable en variables globales, cómo maneja los objetos mutables pasados ​​a sus métodos, y especialmente cómo maneja hashes de opciones).

Finalmente, ser un hilo inseguro es una propiedad transitiva. Todo lo que utiliza algo que no es seguro para subprocesos no está protegido contra subprocesos.