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:
- ¿Qué NO es seguro para subprocesos en ruby / rails? Vs ¿Qué es seguro para subprocesos en ruby / rails?
- ¿Existe una lista de gemas que se sabe que son seguras para subprocesos o viceversa?
- ¿Existe una lista de patrones comunes de código que NO son ejemplos seguros para subprocesos
@result ||= some_method
? - ¿Son
Hash
seguras las estructuras de datos en ruby lang core como, por ejemplo, etc.? - En la resonancia magnética, donde hay una
GVL
/GIL
que significa que solo se puede ejecutar 1 hilo de rubí a la vez, exceptoIO
, ¿nos afecta el cambio de seguridad de hilos?
ruby
multithreading
concurrency
thread-safety
ruby-on-rails-4
Mente curiosa
fuente
fuente
Respuestas:
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 += 1
no es seguro para subprocesos, porque son múltiples operaciones.@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 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 escribirreturn 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 + b
está bien,a = b
también está bien ya.foo(b)
está bien, si el método nofoo
tiene 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:
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
stuff
descriptor 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:
find_stuff
funciona 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 hacecolor = options.delete(:color)
. Ahora laSTANDARD_OPTIONS
constante 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.
fuente
STANDARD_OPTIONS = {...}.freeze
para aumentar en mutaciones superficiales@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@n
igual a 300.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
fuente
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 .
fuente