начиная с Rails 4 , по умолчанию все должно работать в многопоточном окружении. Это означает, что весь код, который мы пишем, И ВСЕ используемые драгоценные камни должны бытьthreadsafe
Итак, у меня есть несколько вопросов по этому поводу:
- что НЕ является потокобезопасным в ruby / rails? Vs Что такое потокобезопасность в ruby / rails?
- Есть ли список драгоценных камней , которые как известно, поточно или наоборот?
- есть ли список общих шаблонов кода, которые НЕ являются потокобезопасным примером
@result ||= some_method
? - Являются ли структуры данных в ядре ruby lang, например,
Hash
потокобезопасными? - На МРТ, где есть
GVL
/,GIL
что означает, что одновременно может выполняться только 1 рубиновый поток, за исключением тогоIO
, влияет ли на нас изменение потока?
ruby
multithreading
concurrency
thread-safety
ruby-on-rails-4
CuriousMind
источник
источник
Ответы:
Ни одна из основных структур данных не является потокобезопасной. Единственное, что мне известно о 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
не означает, что они не являются потокобезопасными, это вполне законно делать в правильном контексте - вместо этого вы должны искать такие вещи, как изменяемое состояние в глобальных переменных, как он обрабатывает изменяемые объекты, переданные его методам, и особенно как он обрабатывает хеши параметров).Наконец, безопасность потоков - это переходное свойство. Все, что использует что-то, что не является потокобезопасным, само по себе не является потокобезопасным.
источник
STANDARD_OPTIONS = {...}.freeze
рейз на мелкие мутации@n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }
[...], значение разделяемой переменной впоследствии не будет детерминированным». - Вы знаете, отличается ли это в разных версиях Ruby? Например, запуск вашего кода на 1.8 дает разные значения@n
, но на 1.9 и более поздних версиях он, кажется, постоянно дает@n
равное 300.В дополнение к ответу Тео, я бы добавил пару проблемных областей, на которые стоит обратить внимание, особенно в Rails, если вы переходите на config.threadsafe!
Переменные класса :
@@i_exist_across_threads
ENV :
ENV['DONT_CHANGE_ME']
Темы :
Thread.start
источник
Это не на 100% правильно. Поточно-ориентированные Rails просто включены по умолчанию. Если вы развертываете сервер приложений с несколькими процессами, например, Passenger (сообщество) или Unicorn, никакой разницы не будет. Это изменение касается только вас, если вы развертываете многопоточную среду, такую как Puma или Passenger Enterprise> 4.0.
В прошлом, если вы хотели выполнить развертывание на сервере многопоточных приложений, вам приходилось включать config.threadsafe , который сейчас используется по умолчанию, потому что все, что оно делало, либо не имело никакого эффекта, либо также применялось к приложению Rails, запущенному в одном процессе ( Корректная ссылка ).
Но если вам нужны все преимущества потоковой передачи Rails 4 и другие возможности многопоточного развертывания в реальном времени, возможно, вам будет интересна эта статья. Как грустно @Theo, для приложения Rails вам просто нужно не изменять статическое состояние во время запроса. Хотя это простая практика, к сожалению, вы не можете быть уверены в этом для каждого найденного драгоценного камня. Насколько я помню, Чарльз Оливер Наттер из проекта JRuby дал несколько советов по этому поводу в этом подкасте.
И если вы хотите написать чисто параллельное программирование на Ruby, где вам понадобятся некоторые структуры данных, к которым обращаются более чем один поток, возможно, вам пригодится гем thread_safe .
источник