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

93

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

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

  1. что НЕ является потокобезопасным в ruby ​​/ rails? Vs Что такое потокобезопасность в ruby ​​/ rails?
  2. Есть ли список драгоценных камней , которые как известно, поточно или наоборот?
  3. есть ли список общих шаблонов кода, которые НЕ являются потокобезопасным примером @result ||= some_method?
  4. Являются ли структуры данных в ядре ruby ​​lang, например, Hashпотокобезопасными?
  5. На МРТ, где есть GVL/,GIL что означает, что одновременно может выполняться только 1 рубиновый поток, за исключением того IO, влияет ли на нас изменение потока?
CuriousMind
источник
2
Вы уверены, что весь код и все драгоценные камни ДОЛЖНЫ быть потокобезопасными? В примечаниях к выпуску говорится, что Rails сам по себе будет потокобезопасным, а не то, что все остальное, что используется с ним,
ДОЛЖНО
Многопоточные тесты были бы наихудшим риском для обеспечения безопасности потоков. Когда вам нужно изменить значение переменной среды в тестовом примере, вы сразу теряете потокобезопасность. Как бы вы с этим справились? И да, все драгоценные камни должны быть потокобезопасными.
Лукас Оберхубер,

Ответы:

110

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

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

Тео
источник
Отличный ответ. Учитывая, что типичное приложение rails является многопроцессорным (как вы описали, много разных пользователей обращаются к одному и тому же приложению), мне интересно, каков предельный риск потоков для модели параллелизма ... Другими словами, насколько «опаснее» он должен работать в потоковом режиме, если вы уже имеете дело с некоторым параллелизмом через процессы?
имбирный лайм 06
2
@ Тео Большое спасибо. Эти постоянные вещи - большая бомба. Это даже небезопасно. Если константа изменяется в одном запросе, это приведет к тому, что последующие запросы увидят измененную константу даже в одном потоке.
Странные
5
Делаем STANDARD_OPTIONS = {...}.freezeрейз на мелкие мутации
glebm
Действительно отличный ответ
Cheyne
3
«Если вы напишете код вроде @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }[...], значение разделяемой переменной впоследствии не будет детерминированным». - Вы знаете, отличается ли это в разных версиях Ruby? Например, запуск вашего кода на 1.8 дает разные значения @n, но на 1.9 и более поздних версиях он, кажется, постоянно дает @nравное 300.
user200783
10

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

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

    @@i_exist_across_threads

  • ENV :

    ENV['DONT_CHANGE_ME']

  • Темы :

    Thread.start

crizCraig
источник
9

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

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

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

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

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

dre-hh
источник