Статьи

Введение в целлулоид, часть I

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

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

Для людей, которые пишут программное обеспечение, понимание параллелизма и правильного его использования внезапно становится невероятно важным. Для того, чтобы масштабироваться вместе с оборудованием, ваш код ДОЛЖЕН использовать параллелизм! Поначалу это казалось достаточно простым; это не может быть так сложно, верно? Ну, это было. Механизм, который (возможно) наиболее популярен для параллелизма, — это потоки, которые чрезвычайно сложны. Несколько небольших ошибок здесь и там могут нанести ущерб. Если вы действительно заинтересованы в подобных вещах, я бы порекомендовал вам немного почитать .

Если вы достаточно смелы, чтобы перейти по нескольким из этих ссылок, не займет много времени, чтобы понять, что управление потоками — это не всегда прогулка в парке. Рассмотрим в качестве примера взаимоблокировку.

(мертвый) Блокировка

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

Эта ситуация известна как тупик.

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

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

Условия гонки

В взаимоблокировке два потока ждут друг друга вечно. Однако у взаимоблокировки есть двоюродный брат, который так же плох, если не хуже.

Состояние гонки возникает, когда два потока пытаются получить доступ и записать в переменную одновременно. Итак, оба потока читают переменную, а затем каждый гоняет, чтобы увидеть, кто может писать в переменную первым / последним.

Это вызывает всевозможные проблемы, потому что это может привести к тому, что изменения одного потока в переменной будут полностью заблокированы другим.

Решение?

Две проблемы, упомянутые выше, являются лишь верхушкой айсберга — есть множество других проблем, которые необходимо решить при использовании потоков.

Примерно в 1980-х люди начали задумываться о том, откуда на самом деле все эти проблемы и как их решать. Они обнаружили, что почти все эти проблемы вызваны общим состоянием (то есть переменными, файлами и т. Д.) И блокировкой. Если вы забудете заблокировать один общий ресурс, вас ждет множество проблем с нитями.

Было предложено несколько решений, одно из которых — четный ввод / вывод, что означает « машина событий» для Rubyists .

Наряду с этим, некоторые яркие ученые придумали новую модель, которая взяла некоторые идеи из квантовой физики. Вместо совместного использования состояния вся система параллелизма будет основана на передаче сообщений. Они назвали это актерской моделью.

Актер модель

В модели актера каждый объект является актером. Каждый участник предназначен для отправки и получения сообщений другим субъектам, а также может создавать других участников, если это необходимо. Основным моментом, вокруг которого все это вращается, является тот факт, что все коммуникации могут быть асинхронными и ни одно состояние не будет разделено между участниками. Это означает, что сообщения могут быть отправлены во время получения других. Кроме того, когда актер отправляет сообщение, ему не всегда приходится ждать ответа. Состояние, которое должно быть разделено между процессами, делается полностью посредством обмена сообщениями.

Если вы не поняли большую часть этого, это прекрасно, просто следуйте.

Конечно, ничего из этого не происходит само по себе. Библиотека под названием (Celluloid) [http://celluloid.io/] делает это возможным, и это то, что мы будем использовать!

Целлулоид

Celluloid привносит в Ruby модель актера, делая невероятно легким написание параллельных приложений.

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

Если вы знакомы с Erlang (это нормально, если нет), Целлулоид заимствует одну из своих самых важных идей: отказоустойчивость. Целлулоид автоматически перезапускает и обрабатывает разбитых актеров, поэтому вам не нужно беспокоиться о каждой последней вещи, которая может пойти не так.

Существует множество других функций (связывание, фьючерсы и т. Д.), Которые делают нарезку ветров проще, но есть несколько вещей, о которых следует помнить.

GIL

«Обычный» или ванильный рубин, к которому мы все привыкли, поддерживается MRI или YARV, которые являются различными типами интерпретаторов / жизненных машин для Ruby.

Теперь, с сомнением, есть проблема с этим переводчиком. Дело в том, что все потоки внутри MRI / YARV на самом деле не параллельны — все выполняется в одном потоке. Это называется глобальной блокировкой интерпретатора. Ruby — не единственный язык с интерпретатором, у которого это есть — так же, как и с Python, и я даже не начинаю работать с PHP.

Когда создается новый поток, результатом является то, что вычисление, которое выполняется, фактически не выполняется в то же время, что и другие операции. Создается иллюзия, заставляющая пользователя думать, что это то, что происходит.

К счастью, есть решение. Просто используйте другой переводчик! Выбирайте:

  • (JRuby) [jruby.org]
  • (Rubini.us) [http://rubini.us/]

Обратите внимание, что если вы решите пойти с одним из вышеперечисленных переводчиков, убедитесь, что вы работаете в режиме 1.9, чтобы быть совместимым с целлулоидом.

Дайвинг в

Давайте начнем с Celluloid, написав небольшого актера — давайте сделаем так, чтобы он прочитал файл, о котором мы ему рассказали, а затем отобразим результаты, когда они понадобятся.

require ‘celluloid’
class FilePutter
include Celluloid
def initialize(filename)
@filename = filename
end
def load_file
@file_contents = File.read @filename
end
def print
p @file_contents
end
end
fp = FilePutter.new «/var/log/kernel.log»
fp.load_file
fp.print

view raw
gistfile1.rb
hosted with ❤ by GitHub

Запустив это (с вашим выбором интерпретатора), вы должны получить дамп журнала ядра (конечно, если вы используете систему POSIX, пользователи Windows могут заменить эту строку любым файлом по своему выбору), за которым следует сообщение от Целлулоид говорит вам, что два актера были уничтожены.

Хорошо, так, что только что произошло?

Мы определили субъект FilePutter и создали его экземпляр, который Celluloid автоматически вставляет в свой собственный поток! Нет разницы в том, как мы вызываем методы; это так же, как мы делаем для обычного класса, и он выводит содержимое файла.

Сначала вызов load_file Не слишком сложно.

Но одна нить не так уж интересна; как насчет пяти? Достаточно просто:

require ‘celluloid’
class FilePutter
include Celluloid
def initialize(filename)
@filename = filename
end
def load_file
@file_contents = File.read @filename
end
def print
p @file_contents
end
end
files = [«/var/log/kernel.log», «/var/log/system.log», «/var/log/ppp.log», «/var/log/secure.log»]
files.each do |file|
fp = FilePutter.new file
fp.load_file
fp.print
end

view raw
gistfile1.rb
hosted with ❤ by GitHub

И точно так же мы создали пять потоков, каждый из которых читает файлы в массиве «files».

Но все методы, которые мы вызывали до сих пор, были вызваны синхронно , что означает, что мы должны ждать их окончания, прежде чем продолжить. Что если мы просто отодвинем их в сторону и двинемся дальше?

Вот где действительно сияет целлулоид:

require ‘celluloid’
class FilePutter
include Celluloid
def initialize(filename)
@filename = filename
end
def load_file_and_print
@file_contents = File.read @filename
p @file_contents
end
end
files = [«/var/log/kernel.log», «/var/log/system.log», «/var/log/ppp.log», «/var/log/secure.log»]
files.each do |file|
fp = FilePutter.new file
fp.load_file_and_print!
end

view raw
gistfile1.rb
hosted with ❤ by GitHub

Вот где это становится интересным. Прежде всего, мы объединили загрузку файла и печать содержимого в один метод, а именно load_file_and_print Затем обратите внимание, что внутри цикла над массивом файлов мы не вызываем load_file_and_printload_file_and_print! (т.е. с треском).

Завершение

Когда мы вызываем данный метод с ударом, Celluloid запускает этот вызов асинхронно, позволяя нашей программе двигаться вперед, не дожидаясь загрузки файла или печати.

Как мы знаем, методы с восклицательным знаком в конце обычно отмечаются как «опасные» в Ruby. В качестве примера:

a = «hello, world!\n«
a.strip!
p a

view raw
gistfile1.rb
hosted with ❤ by GitHub

Это меняет само значение «а», что может привести к проблемам.

Тот же самый случай присутствует с методами взрыва в целлулоиде; асинхронная доставка сообщений не идеальна. Сообщение может быть не доставлено, субъект может не отвечать и т. Д.

Но как вы узнаете, когда это произойдет?

Кроме того, потоки никогда не бывают полностью независимы друг от друга — как они могут общаться друг с другом?

Это, а также некоторые другие важные особенности и тонкости актерской модели появятся во второй части, так что следите за обновлениями!

Если вы считаете, что эта статья идет слишком медленно, вы будете удовлетворены второй частью 🙂

Пожалуйста, задавайте вопросы, если у вас есть какие-либо в разделе комментариев.