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

93

a partir de Rails 4 , todo debería ejecutarse en un entorno de subprocesos de forma predeterminada. Lo que esto significa es que todo el código que escribimos Y TODAS las gemas que usamos deben serthreadsafe

entonces, tengo algunas preguntas sobre esto:

  1. ¿Qué NO es seguro para subprocesos en ruby ​​/ rails? Vs ¿Qué es seguro para subprocesos en ruby ​​/ rails?
  2. ¿Existe una lista de gemas que se sabe que son seguras para subprocesos o viceversa?
  3. ¿Existe una lista de patrones comunes de código que NO son ejemplos seguros para subprocesos @result ||= some_method?
  4. ¿Son Hashseguras las estructuras de datos en ruby ​​lang core como, por ejemplo, etc.?
  5. En la resonancia magnética, donde hay una GVL/GIL que significa que solo se puede ejecutar 1 hilo de rubí a la vez, excepto IO, ¿nos afecta el cambio de seguridad de hilos?
Mente curiosa
fuente
2
¿Estás seguro de que todo el código y todas las gemas DEBEN ser seguras para subprocesos? Lo que dicen las notas de la versión es que Rails en sí mismo será seguro para subprocesos, no que todo lo demás que se use con él TIENE que serlo
cautiva el
Las pruebas de subprocesos múltiples serían el peor riesgo posible de seguridad para subprocesos. Cuando tiene que cambiar el valor de una variable de entorno alrededor de su caso de prueba, instantáneamente no es seguro para subprocesos. ¿Cómo solucionaría eso? Y sí, todas las gemas deben ser seguras.
Lukas Oberhuber

Respuestas:

110

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

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 todavía 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 } } }por ejemplo, mutando una variable compartida de varios subprocesos, 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 un solo núcleo, no cambia las cuestiones fundamentales de escribir programas concurrentes correctos.

Incluso si MRI hubiera sido de un solo subproceso 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 en las que las cosas suceden en un orden no determinista y una devolución de llamada golpea el resultado de otra. Los sistemas asíncronos de un solo subproceso son más fáciles de razonar, pero no están libres de problemas de concurrencia. Solo piense en una aplicación con varios usuarios: si dos usuarios presionan editar en una publicación de Stack Overflow más o menos al mismo tiempo, dedique un tiempo a editar la publicación y luego presione guardar, cuyos cambios serán vistos por un tercer usuario más adelante cuando leer la misma publicación?

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

No conozco ninguna lista autorizada de declaraciones seguras para subprocesos y no seguras para subprocesos para Ruby, pero hay una regla general simple: si una expresión solo realiza una operación (sin efectos secundarios), probablemente sea segura para subprocesos. Por ejemplo: a + bestá bien, a = btambién está bien y a.foo(b)está bien, si el método no footiene efectos secundarios (dado que casi cualquier cosa en Ruby es una llamada a un método, incluso una asignación en muchos casos, esto también se aplica a los otros ejemplos). Los efectos secundarios en este contexto significan cosas que cambian de estado. nodef foo(x); @x = x; end está 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, incluidas la matriz, el hash y la cadena, son mutables. Es muy fácil filtrar accidentalmente una parte de su estado, y cuando esa parte es mutable, las cosas pueden estropearse mucho. Considere 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 stuffdescriptor de acceso. 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 otro lugar, 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_stufffunciona bien la primera vez que se usa, pero devuelve algo más la segunda vez. ¿Por qué? losload_things método piensa que posee el hash de opciones que se le pasa, y lo hace color = options.delete(:color). Ahora la STANDARD_OPTIONSconstante ya no tiene el mismo valor. Las constantes solo son constantes en lo que hacen referencia, no garantizan la constancia de las estructuras de datos a las que hacen referencia. Piense en lo que sucederí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 acceden varios subprocesos, estructuras de datos como hashes y matrices a las que acceden varios subprocesos), la seguridad de los subprocesos no es tan difícil. Intente minimizar las partes de su aplicación a las que se accede simultáneamente y concentre sus esfuerzos allí. IIRC, en una aplicación Rails, se crea un nuevo objeto de controlador para cada solicitud, por lo que solo lo utilizará un único hilo, y lo mismo ocurre con cualquier objeto modelo que cree a partir de ese controlador. Sin embargo, Rails también fomenta el uso de variables globales ( User.find(...)utiliza la variable globalUser, puede pensar en ella como solo una clase, y es una clase, pero también es un espacio de nombres para variables globales), algunas de estas son seguras porque son de solo lectura, pero a veces guarda cosas en estas variables globales porque es conveniente. Tenga mucho cuidado cuando use cualquier cosa que sea accesible globalmente.

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

Finalmente, no ser seguro para los subprocesos es una propiedad transitiva. Todo lo que use algo que no sea seguro para subprocesos no lo es en sí mismo.

Theo
fuente
Gran respuesta. Teniendo en cuenta que una aplicación de rieles típica es multiproceso (como describiste, muchos usuarios diferentes acceden a la misma aplicación), me pregunto cuál es el riesgo marginal de los subprocesos para el modelo de concurrencia ... En otras palabras, cuánto más "peligroso" ¿Se ejecutará en modo de subprocesos si ya está lidiando con cierta concurrencia a través de procesos?
gingerlime
2
@Theo Muchas gracias. Ese material constante es una gran bomba. Ni siquiera es seguro para el proceso. Si la constante se cambia en una solicitud, hará que las solicitudes posteriores vean la constante modificada incluso en un solo hilo. Las constantes de Ruby son raras
rubish
5
Hacer STANDARD_OPTIONS = {...}.freezepara aumentar en mutaciones superficiales
glebm
Realmente gran respuesta
Cheyne
3
"Si escribe código como @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }[...], el valor de la variable compartida después no es determinista". - ¿Sabes si esto difiere entre las versiones de Ruby? Por ejemplo, ejecutar su código en 1.8 da diferentes valores de @n, pero en 1.9 y posteriores parece dar consistentemente @nigual a 300.
user200783
10

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

  • Variables de clase :

    @@i_exist_across_threads

  • ENV :

    ENV['DONT_CHANGE_ME']

  • Hilos :

    Thread.start

crizCraig
fuente
9

a partir de Rails 4, todo debería ejecutarse en un entorno de subprocesos de forma predeterminada

Esto no es 100% correcto. Los carriles seguros para subprocesos están activados de forma predeterminada. Si implementa en un servidor de aplicaciones multiproceso como Passenger (comunidad) o Unicorn, no habrá ninguna diferencia. Este cambio solo le concierne si lo implementa en un entorno multiproceso como Puma o Passenger Enterprise> 4.0

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

Pero si desea todos los beneficios de transmisión de Rails 4 y otras cosas en tiempo real de la implementación de subprocesos múltiples, entonces tal vez encuentre este artículo interesante. Como @Theo triste, para una aplicación Rails, en realidad solo tiene que omitir el estado estático mutante durante una solicitud. Si bien esta es una práctica simple de seguir, desafortunadamente no puede estar seguro de esto para cada gema que encuentre. Por lo que recuerdo, Charles Oliver Nutter del proyecto JRuby tenía algunos consejos al respecto en este podcast.

Y si desea escribir una programación Ruby concurrente pura, en la que necesitaría algunas estructuras de datos a las que accede más de un hilo, tal vez encuentre útil la gema thread_safe .

dre-hh
fuente