Ответ 1
Ни одна из основных структур данных не является потокобезопасной. Единственное, что я знаю о кораблях с Ruby, - это реализация очереди в стандартной библиотеке (require 'thread'; q = Queue.new
).
MRI GIL не спасает нас от проблем безопасности потоков. Он только гарантирует, что два потока не могут одновременно запускать Ruby-код, то есть на двух разных процессорах в одно и то же время. Темы могут быть приостановлены и возобновлены в любой точке вашего кода. Если вы пишете код типа @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }
, например. изменяя общую переменную из нескольких потоков, значение общей переменной впоследствии не является детерминированным. GIL - это более или менее симуляция одноядерной системы, она не меняет фундаментальных проблем написания правильных параллельных программ.
Даже если MRI был однопоточным, например Node.js, вам все равно придется думать о concurrency. Пример с добавленной переменной будет работать нормально, но вы все равно можете получить условия гонки, когда вещи происходят в недетерминированном порядке, а один обратный вызов сжимает результат другого. Одиночные асинхронные системы легче рассуждать, но они не свободны от проблем concurrency. Просто подумайте о приложении с несколькими пользователями: если два пользователя нажимают на редактирование в столбце "Переполнение стека" более или менее одинаково, потратите некоторое время на редактирование сообщения, а затем нажмите "Сохранить", изменения которого будут замечены третьим пользователем позже, когда они прочитал этот же пост?
В 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
Экземпляр этого класса может быть разделен между потоками, и они могут безопасно добавлять к нему вещи, но там ошибка concurrency (это не единственная): внутреннее состояние объекта протекает через аксессуар stuff
, Помимо проблематичности с точки зрения инкапсуляции, он также открывает банку червей concurrency. Возможно, кто-то берет этот массив и передает его куда-то еще, и этот код, в свою очередь, думает, что теперь он владеет этим массивом и может делать с ним все, что захочет.
Другой классический пример 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
не означает, что они не являются потокобезопасными, что совершенно законно нужно делать в правильном контексте - вместо этого вы должны искать такие вещи, как изменчивое состояние в глобальных переменных, как обрабатывать изменяемые объекты, переданные его методам, и особенно он обрабатывает хэши опций).
Наконец, быть небезопасным является транзитивным свойством. Все, что использует то, что не является потокобезопасным, само по себе не является безопасным для потоков.