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 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 + b
está bien, a = b
también está bien y a.foo(b)
está bien, si el método no foo
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:
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 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:
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é? 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_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.