2013-09-30 5 views
1

Желая убедиться, что мы используем правильную синхронизацию (и не более чем необходимо) при написании потокового кода в JRuby; в частности, в приложении Rails с экземплярами Rails.Использование инициализации threadafe в JRuby gem

ОБНОВЛЕНИЕ: Настоящим образом отредактирован этот вопрос, чтобы быть предельно ясным и использовать последний код, который мы реализуем. Этот код использует жемчужину atomic, написанную @headius (Charles Nutter) для JRuby, но не уверен, что это абсолютно необходимо или в каких случаях это необходимо для того, что мы пытаемся сделать здесь.

Вот что у нас есть, это излишний (что означает, мы закончили/uber-engineering это) или, может быть, неверно?

ourgem.rb:

require 'atomic' # gem from @headius 

SUPPORTED_SERVICES = %w(serviceABC anotherSvc andSoOnSvc).freeze 

module Foo 

    def self.included(cls) 
    cls.extend(ClassMethods) 
    cls.send :__setup 
    end 

    module ClassMethods 
    def get(service_name, method_name, *args) 
     __cached_client(service_name).send(method_name.to_sym, *args) 
     # we also capture exceptions here, but leaving those out for brevity 
    end 

    private 

    def __client(service_name) 
     # obtain and return a client handle for the given service_name 
     # we definitely want to cache the value returned from this method 
     # **AND** 
     # it is a requirement that this method ONLY be called *once PER service_name*. 
    end 

    def __cached_client(service_name) 
     @@_clients.value[service_name] 
    end 

    def __setup 
     @@_clients = Atomic.new({}) 
     @@_clients.update do |current_service| 
     SUPPORTED_SERVICES.inject(Atomic.new({}).value) do |memo, service_name| 
      if current_services[service_name] 
      current_services[service_name] 
      else 
      memo.merge({service_name => __client(service_name)}) 
      end 
     end 
     end 
    end 
    end 
end 

client.rb:

require 'ourgem' 

class GetStuffFromServiceABC 
    include Foo 

    def self.get_some_stuff 
    result = get('serviceABC', 'method_bar', 'arg1', 'arg2', 'arg3') 
    puts result 
    end 
end 

Резюме выше: мы имеем @@_clients (изменяемый переменный класс, держащий Hash клиентов), который мы только хотим заполняйте ONCE для всех доступных сервисов, которые вводятся в ключ для имени службы.

Поскольку хэш в переменном классе (и, следовательно, поточно?), Мы гарантировали, что призыв к __client не будет работать более чем один раз за имя службы (даже если Пума инстанцирования нескольких потоков с этим классом для обслуживания всех запросов от разных пользователей)? Если переменная класса является потокобезопасной (таким образом), то, возможно, Atomic.new({}) не требуется?

Кроме того, следует ли вместо этого использовать Atomic.new(ThreadSafe::Hash)? Или снова, разве это не нужно?

Если нет (что означает: вы думаете, мы сделать нужны Atomic.new сек, по крайней мере, и, возможно, также в ThreadSafe::Hash), то почему бы не второй (или третий и т.д.) поток прерываний между Atomic.new(nil) а @@_clients.update do ... означает, что Atomic.new s из КАЖДОЙ нити будет КАЖДОЙ создать два (отдельных) объекта?

Благодарим вас за любые советы по безопасности нитей, мы не видим никаких вопросов по SO, которые напрямую касаются этой проблемы.

+0

кажется немного утомительным, но, возможно, не все требования ясны и видны здесь: действительно зависит от использования, как они используются? для первого примера, что такое стек, вызывающий 'initialize', как для второй загрузки/требующей файла, является поточно-безопасным, если вы считаете, что:' @@ _ thrift_client = __thrift_client' ... прекрасно – kares

+0

Спасибо @kares Я отредактировал вопрос значительно. Идея заключается в том, что когда этот класс сначала «включает» в клиенте, мы будем выполнять вызов '__client' * только один раз за одно имя службы. После того, как мы собрали все клиентские дескрипторы обслуживания и кэшировали их, клиент может вызывать эти службы с помощью методов так часто, как им нравится, и мы всегда будем использовать один и тот же дескриптор клиента для каждой другой службы, которую они вызывают.(Если вы хотите знать фон: клиентские дескрипторы - это набор различных сервисов на основе бережливости, которые предоставляются через сервис Zookeeper.) – likethesky

+0

Я хочу указать несколько релевантных ссылок на мой вопрос, один из которых касается [переменные класса и безопасность потоков] (http://stackoverflow.com/questions/9558192/thread-safety-class-variables-in-ruby) и еще один о [потокобезопасных хешах Ruby] (http: // stackoverflow. com/questions/9265879/fast-thread-safe-ruby-hash-with-strong-read-bias), re: этот последний, я бы предпочел использовать версию Java, а не Ruby, так как я использую JRuby уже (и не очень нравится идея использования нестандартного кода для чего-то подобного). Но мой вопрос остается: действительно ли эти методы необходимы? – likethesky

ответ

3

Просто дружеский совет, прежде чем я пытаться решать вопросы, вы поднимаете здесь:

Этот вопрос, и сопровождающий код, наводит на мысль, что вы (пока) не имеют твердое понимание из проблемы, связанные с написанием многопоточного кода. Я рекомендую вам подумать дважды, прежде чем принимать решение о создании многопоточного приложения для использования в производстве. Почему вы действительно хотите использовать Puma? Это для производительности? Будет ли ваше приложение обрабатывать много длительных запросов ввода-вывода (например, загрузка/загрузка больших файлов) одновременно? Или (как и многие приложения) будет ли он в основном обрабатывать короткие запросы с привязкой к процессору?

Если ответ «короткий/CPU-bound», то у вас мало шансов выиграть от использования Puma. Было бы лучше использовать несколько однопоточных серверных процессов. Потребление памяти будет выше, но вы сохраните свое здравомыслие. Написание правильного многопоточного кода дьявольски усердно, и даже эксперты делают ошибки. Если ваш успех в бизнесе, безопасность работы и т. Д. Зависят от того, что многопоточный код работает и работает правильно, вы будете причинять себе массу ненужной боли и душевных страданий.

В стороне, позвольте мне попытаться разгадать некоторые вопросы, поднятые в вашем вопросе. Существует так много, что трудно понять, с чего начать. Вы можете захотеть налить себе холодный или горячий напиток по вашему выбору, прежде чем садиться, чтобы прочитать этот трактат:

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

Обратите внимание, что, когда ваши потоки только чтение данных, не изменяя его, или при работе с общими апатридов объектов, не вопрос «безопасности потока».

Другое определение «поточно-безопасное», которое, вероятно, лучше подходит для вашей ситуации, связано с операциями, которые влияют на внешний мир (в основном I/O). Вы можете захотеть, чтобы некоторые операции выполнялись только один раз или выполнялись в определенном порядке. Если код, выполняющий эти операции, выполняется по нескольким потокам, они могут происходить в несколько раз больше, чем это необходимо, или в другом порядке, чем требуется, если только вы не сделаете что-то, чтобы это предотвратить.

Оказывается, ваш метод __setup вызывается только в том случае, если загружается ourgem.rb. Насколько я знаю, даже если несколько потоков require тот же самый файл одновременно, MRI позволит только один поток загружать файл. Я не знаю, является ли JRuby одинаковым. Но в любом случае, если ваши исходные файлы загружаются более одного раза, это является симптомом более глубокой проблемы. Их нужно загружать только один раз в одном потоке. Если ваше приложение обрабатывает запросы на несколько потоков, эти потоки должны быть запущены до после загрузки приложений, а не раньше. Это единственный разумный способ сделать что-то.

Предполагая, что все в порядке, ourgem.rb будет загружен с использованием одного потока. Это означает, что только __setup будет вызываться только одним потоком. В этом случае не стоит беспокоиться о безопасности потоков (о том, как идет инициализация вашего «кэша клиента»).

Даже если __setup должен был вызываться одновременно несколькими потоками, ваш атомный код не будет делать то, что вы думаете. Прежде всего, вы используете Atomic.new({}).value. Это обертывает хеш в атомной ссылке, затем разворачивает его, поэтому вы просто возвращаетесь к хешу. Это не-op. Вместо этого вы могли бы написать {}.

Во-вторых, ваш звонок Atomic#update будет не предотвратить запуск кода инициализации более одного раза. Чтобы понять это, вам нужно знать, что на самом деле делает Atomic.

Позвольте мне вытащить старый, усталый пример «увеличить общий счетчик». Представьте, что следующий код работает на 2 потоках:

i += 1 

Мы все знаем, что здесь может быть не так. Вы можете получить следующую последовательность событий:

  1. Резьба А читает i и увеличивает ее.
  2. Резьба B читает i и увеличивает его.
  3. Резьба A записывает добавочное значение обратно в i.
  4. Резьба B записывает добавочное значение обратно в i.

Итак, мы потеряли обновление, не так ли? Но что, если мы сохраним значение счетчика в атомной ссылке и используем Atomic#update? Тогда это будет так:

  1. Резьба А читает i и увеличивает ее.
  2. Резьба B читает i и увеличивает его.
  3. Тема А пытается записать увеличенное значение обратно на i и успешно.
  4. Thread B пытается записать свое добавочное значение обратно в i и сбой, поскольку значение уже изменилось.
  5. Резьба B снова читает i и увеличивает его.
  6. Резьба B пытается снова записать свое добавочное значение обратно на i и успешно исполняется на этот раз.

Есть ли у вас идея? Atomicnever останавливает 2 потока одновременного запуска одного и того же кода. Что это значит, делает do, заставляет некоторые потоки повторять блок #update, когда это необходимо, во избежание потери обновлений.

Если ваша цель - обеспечить, чтобы ваш код инициализации выполнялся только один раз, использование Atomic является очень неуместным выбором. Во всяком случае, он мог бы запустить его еще раз, а не меньше (из-за попыток).

Итак, вот что. Но если вы по-прежнему со мной здесь, меня больше беспокоит, являются ли ваши «клиентские» объекты потокобезопасными. У них есть какое-то изменчивое состояние? Поскольку вы их кешируете, кажется, что их инициализация должна быть медленной. Как бы то ни было, если вы используете блокировки, чтобы сделать их потокобезопасными, вы не сможете получать что-либо от кеширования и делиться ими между потоками. Ваш многопоточный сервер может быть сведен к тому, что фактически является излишне сложным однопоточным сервером.

Если объекты-клиенты не имеют изменяемого состояния, это хорошо для вас. Вы можете быть «свободным и легким» и делиться ими между потоками без проблем. Если у них есть изменяемое состояние, но инициализация их происходит медленно, я бы рекомендовал кэшировать один объект в потоке, поэтому они никогда не будут использоваться совместно. Thread[] твой друг.

+0

Спасибо вам, любезно @Alex D! Итак, я полагаю, вы ответили на мой вопрос. Согласно документу [JRuby doc, 4th bullet: «Определение или изменение констант и переменных класса»] (https://github.com/jruby/jruby/wiki/Concurrency-in-jruby#thread-safety) JRuby * делает * гарантию что только один поток будет когда-либо вызывать настройку, когда включен класс (и набор переменных класса). Поэтому, по сути, мы не беспокоимся об использовании Atomic (неправильное решение, как вы указываете) или даже о [высокопроизводительной блокировке чтения/записи] (https://github.com/alexdowad/showcase/blob /master/ruby-threads/read_write_lock.rb). – likethesky

+0

Чтобы ответить на ваши дальнейшие проблемы, да, наши объекты-клиенты не слишком дороги, но они являются ограниченными ресурсами, и мы не можем создать их для каждого потока. Объекты Java создают клиентский дескриптор, и эти .jars гарантированно будут полностью реентерабельными и потокобезопасными (т. Е. Мы будем когда-либо «читать» дескриптор и вызывать эти .jars, как только приложение будет инициализировано, повторное использование дескрипторов над & над). – likethesky

+0

Соответственно, я сократил наш метод __setup до следующего: https://gist.github.com/likethesky/712882 Поскольку кажется очевидным, что не требуется код Atomic, ThreadSafe :: Hash или read_write_lock. Это связано с тем, что, поскольку JRuby гарантирует, что, когда этот класс включен во время загрузки, никакие другие потоки не будут запускать __setup одновременно, простой «|| =» будет защищать это достаточно (и threadsfely, поскольку только один поток работает в то время __setup бег). – likethesky

Смежные вопросы