Ни одна из основных структур данных не является потокобезопасной. Единственное, что мне известно о Ruby, это реализация очереди в стандартной библиотеке ( require 'thread'; q = Queue.new
).
GIL MRI не избавляет нас от проблем безопасности потоков. Это только гарантирует, что два потока не могут запускать код Ruby одновременно , то есть на двух разных процессорах в одно и то же время. Потоки по-прежнему можно приостанавливать и возобновлять в любой момент вашего кода. Если вы пишете код, @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }
например, изменяя общую переменную из нескольких потоков, значение общей переменной впоследствии не будет детерминированным. GIL - это более или менее симуляция одноядерной системы, он не меняет фундаментальных проблем написания корректных параллельных программ.
Даже если бы MRI был однопоточным, как Node.js, вам все равно пришлось бы думать о параллелизме. Пример с увеличивающейся переменной будет работать нормально, но вы все равно можете получить условия гонки, когда все происходит в недетерминированном порядке, и один обратный вызов затирает результат другого. Об однопоточных асинхронных системах легче рассуждать, но они не свободны от проблем параллелизма. Подумайте о приложении с несколькими пользователями: если два пользователя нажимают на редактирование в сообщении Stack Overflow более или менее в одно и то же время, потратьте некоторое время на редактирование сообщения, а затем нажмите «Сохранить», изменения которого будут видны третьим пользователям позже, когда они читали тот же пост?
В Ruby, как и в большинстве других параллельных сред выполнения, все, что связано с несколькими операциями, не является потокобезопасным. @n += 1
не является потокобезопасным, потому что это несколько операций. @n = 1
является потокобезопасным, потому что это одна операция (это много операций под капотом, и у меня, вероятно, возникли бы проблемы, если бы я попытался подробно описать, почему он «потокобезопасен», но в конечном итоге вы не получите противоречивых результатов от назначений ). @n ||= 1
, нет, и никакая другая сокращенная операция + присваивание тоже. Одна ошибка, которую я делал много раз, - это писать return unless @started; @started = true
, что вообще не является потокобезопасным.
Я не знаю какого-либо авторитетного списка потокобезопасных и небезопасных операторов для Ruby, но есть простое практическое правило: если выражение выполняет только одну операцию (без побочных эффектов), оно, вероятно, является потокобезопасным. Например: a + b
это нормально, a = b
тоже нормально и a.foo(b)
нормально, если метод не foo
имеет побочных эффектов (поскольку почти все в Ruby является вызовом метода, во многих случаях даже присваиванием, это относится и к другим примерам). Побочные эффекты в этом контексте означают вещи, которые меняют состояние. def foo(x); @x = x; end
это не побочный эффект бесплатно.
Одна из самых сложных вещей при написании поточно-безопасного кода в Ruby заключается в том, что все основные структуры данных, включая массив, хэш и строку, являются изменяемыми. Очень легко случайно пропустить часть вашего состояния, а когда эта часть является изменяемой, все может действительно испортиться. Рассмотрим следующий код:
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
Экземпляр этого класса может совместно использоваться потоками, и они могут безопасно добавлять в него что-либо, но есть ошибка параллелизма (она не единственная): внутреннее состояние объекта просачивается через метод stuff
доступа. Помимо проблем с точки зрения инкапсуляции, он также открывает множество червей параллелизма. Может быть, кто-то возьмет этот массив и передаст его в другое место, а этот код, в свою очередь, считает, что теперь он владеет этим массивом и может делать с ним все, что захочет.
Еще один классический пример Ruby:
STANDARD_OPTIONS = {:color => 'red', :count => 10}
def find_stuff
@some_service.load_things('stuff', STANDARD_OPTIONS)
end
find_stuff
работает нормально при первом использовании, но возвращает что-то еще во второй раз. Зачем? load_things
Метод бывает думать , что это имеет хеш опций , переданный ему, и делает color = options.delete(:color)
. Теперь у STANDARD_OPTIONS
константы больше нет того же значения. Константы постоянны только в том, на что они ссылаются, они не гарантируют постоянство структур данных, на которые они ссылаются. Подумайте, что бы произошло, если бы этот код запускался одновременно.
Если вы избегаете разделяемого изменяемого состояния (например, переменные экземпляра в объектах, к которым обращаются несколько потоков, структуры данных, такие как хэши и массивы, к которым обращаются несколько потоков), безопасность потоков не так уж и сложна. Постарайтесь свести к минимуму части вашего приложения, к которым осуществляется одновременный доступ, и сконцентрируйте свои усилия на них. IIRC, в приложении Rails новый объект контроллера создается для каждого запроса, поэтому он будет использоваться только одним потоком, и то же самое касается любых объектов модели, которые вы создаете из этого контроллера. Однако Rails также поощряет использование глобальных переменных ( User.find(...)
использует глобальную переменнуюUser
, вы можете думать об этом только как о классе, и это класс, но это также пространство имен для глобальных переменных), некоторые из них безопасны, потому что они доступны только для чтения, но иногда вы сохраняете что-то в этих глобальных переменных, потому что это удобно. Будьте очень осторожны при использовании всего, что доступно во всем мире.
Уже довольно долгое время можно запускать Rails в многопоточных средах, поэтому, не будучи экспертом по Rails, я бы пошел еще дальше и сказал, что вам не нужно беспокоиться о безопасности потоков, когда речь идет о самом Rails. Вы по-прежнему можете создавать приложения Rails, которые не являются потокобезопасными, выполнив некоторые из упомянутых выше действий. Когда дело доходит до других драгоценных камней, они предполагают, что они не являются потокобезопасными, если они не говорят, что они есть, и если они говорят, что они, предполагают, что это не так, и просматривают их код (но только потому, что вы видите, что они делают что-то вроде@n ||= 1
не означает, что они не являются потокобезопасными, это вполне законно делать в правильном контексте - вместо этого вы должны искать такие вещи, как изменяемое состояние в глобальных переменных, как он обрабатывает изменяемые объекты, переданные его методам, и особенно как он обрабатывает хеши параметров).
Наконец, безопасность потоков - это переходное свойство. Все, что использует что-то, что не является потокобезопасным, само по себе не является потокобезопасным.