Как узнать, что НЕ является потокобезопасным в рубине?

начиная с Rails 4, все по умолчанию должно запускаться в потоковой среде. Это означает, что весь код, который мы пишем И ВСЕ, мы должны использовать threadsafe

поэтому у меня мало вопросов по этому поводу:

  • что НЕ является потокобезопасным в рубинах/рельсах? Vs Что такое потокобезопасность в рубинах/рельсах?
  • Есть ли список драгоценных камней, которые, как известно, являются потокобезопасными или наоборот?
  • Есть ли список общих шаблонов кода, которые не являются потокобезопасными примерами @result ||= some_method?
  • Являются ли структуры данных в ядре ruby ​​lang такими, как Hash и т.д. threadsafe?
  • На MRI, где есть GVL/GIL, что означает, что только 1 рубиновый поток может запускаться за раз, за ​​исключением IO, влияет ли на нас поточное изменение?

Ответ 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 не означает, что они не являются потокобезопасными, что совершенно законно нужно делать в правильном контексте - вместо этого вы должны искать такие вещи, как изменчивое состояние в глобальных переменных, как обрабатывать изменяемые объекты, переданные его методам, и особенно он обрабатывает хэши опций).

Наконец, быть небезопасным является транзитивным свойством. Все, что использует то, что не является потокобезопасным, само по себе не является безопасным для потоков.

Ответ 2

В дополнение к ответу Тео, я бы добавил пару проблемных областей для поиска в Rails специально, если вы переключитесь на config.threadsafe!

  • Переменные класса:

    @@i_exist_across_threads

  • ENV

    ENV['DONT_CHANGE_ME']

  • Темы

    Thread.start

Ответ 3

начиная с Rails 4, все по умолчанию будет выполняться в потоковой среде.

Это не на 100% правильно. По умолчанию Threadsafe Rails включен. Если вы все еще используете многопроцессорный сервер приложений, такой как пассажир (сообщество) или единорог, не будет никакой разницы. Это изменение касается только вас, если вы развертываете многопоточную среду, например Puma или Passenger Enterprise > 4.0

В прошлом, если вы хотели развернуть на многопоточном сервере приложений, вам пришлось включить config.threadsafe, который по умолчанию теперь, потому что все, что у него было, не имело никаких эффектов или также применялось к приложению rails, работающему в одном процессе (Prooflink).

Но если вы действительно хотите использовать все преимущества 4 streaming рельсов и другие материалы многопоточного развертывания в режиме реального времени то, возможно, вы найдете эту статью интересной. Поскольку @Theo грустно, для приложения rails вам просто нужно отказаться от мутирующего статического состояния во время запроса. Хотя это простая практика, чтобы следовать, к сожалению, вы не можете быть уверены в этом для каждого драгоценного камня, который вы найдете. Насколько я помню, Чарльз Оливер Нуттер из проекта Jruby имел несколько подсказок об этом в этом подкасте.

И если вы хотите написать чисто параллельное рубиновое программирование, где вам понадобятся некоторые структуры данных, к которым обращаются более чем один поток, вы, возможно, найдете thread_safe драгоценный камень полезный