Зачем нам нужны волокна
для волокон мы получили классический пример: генерация чисел Фибоначчи
fib = Fiber.new do
x, y = 0, 1
loop do
Fiber.yield y
x,y = y,x+y
end
end
зачем нам нужны волокна здесь? Я могу переписать это с тем же Proc (закрытие, на самом деле)
def clsr
x, y = 0, 1
Proc.new do
x, y = y, x + y
x
end
end
так
10.times { puts fib.resume }
и
prc = clsr
10.times { puts prc.call }
вернет точно такой же результат.
Итак, каковы преимущества волокон. Что я могу написать с волокнами, которые я не могу сделать с лямбдами и другими крутыми функциями Ruby?
2 ответов:
волокна-это то, что вы, вероятно, никогда не будете использовать непосредственно в коде уровня приложения. Они представляют собой примитив управления потоком, который можно использовать для построения других абстракций, которые затем используются в коде более высокого уровня.
вероятно, #1 Использование волокон в Ruby заключается в реализации
Enumerators, которые являются основным классом Ruby в Ruby 1.9. Это невероятно полезное.в Ruby 1.9, если вы вызываете почти любой метод итератора в основных классах, без проходя блок, он вернет
Enumerator.irb(main):001:0> [1,2,3].reverse_each => #<Enumerator: [1, 2, 3]:reverse_each> irb(main):002:0> "abc".chars => #<Enumerator: "abc":chars> irb(main):003:0> 1.upto(10) => #<Enumerator: 1:upto(10)>эти
EnumeratorS-это перечислимые объекты, и ихeachметоды дают элементы, которые были бы получены с помощью исходного метода итератора, если бы он был вызван с блоком. В Примере, который я только что дал, перечислитель возвращаетсяreverse_eachестьeachметод, который дает 3,2,1. Перечислитель, возвращенныйcharsдает "c","b", " a " (и так далее). Но, в отличие от исходного итератора метод, перечислитель также может возвращать элементы по одному, если вы вызываетеnextна нее неоднократно:irb(main):001:0> e = "abc".chars => #<Enumerator: "abc":chars> irb(main):002:0> e.next => "a" irb(main):003:0> e.next => "b" irb(main):004:0> e.next => "c"возможно, вы слышали о "внутренних итераторах" и "внешних итераторах" (хорошее описание обоих приведено в книге шаблонов проектирования "Банда четырех"). Приведенный выше пример показывает, что Перечислители можно использовать для преобразования внутреннего итератора во внешний.
это один из способов сделать свои собственные счетчики:
class SomeClass def an_iterator # note the 'return enum_for...' pattern; it's very useful # enum_for is an Object method # so even for iterators which don't return an Enumerator when called # with no block, you can easily get one by calling 'enum_for' return enum_for(:an_iterator) if not block_given? yield 1 yield 2 yield 3 end endдавайте попробуем это:
e = SomeClass.new.an_iterator e.next # => 1 e.next # => 2 e.next # => 3минуточку... не странно ли? Вы написали
yieldзаявления вan_iteratorкак прямой код, но перечислитель может запускать их один раз. В перерывах между звонками наnextисполнениеan_iterator"заморожен". Каждый раз, когда вы звонитеnext, продолжает работать до следующегоyieldзаявление, а затем снова "зависает".можете ли вы догадаться, как это реализуется? Перечислитель обертывает вызов к
an_iteratorв волокне, и проходит блок который приостанавливает волокна. Так что каждый разan_iteratorуступает блоку, волокно, на котором он работает, приостанавливается, и выполнение продолжается на основном потоке. В следующий раз, когда вы позвонитеnext, он передает управление волокна, возвращает блок иan_iteratorпродолжается там, где он остановился.было бы поучительно подумать о том, что потребуется для этого без волокон. Каждый класс, который хотел обеспечить как внутренние, так и внешние итераторы должны содержать явный код для отслеживания состояния между вызовами
next. Каждый вызов next должен был бы проверить это состояние и обновить его перед возвратом значения. С волокнами, мы можем автоматически преобразуйте любой внутренний итератор во внешний.это не имеет отношения к волокнам persay, но позвольте мне упомянуть еще одну вещь, которую вы можете сделать с Перечислителями: они позволяют применять перечисляемые более высокого порядка методы для других итераторов, отличных от
each. Подумайте об этом: обычно все перечисляемые методы, включаяmap,select,include?,injectи так далее все работа над элементами, полученнымиeach. Но что делать, если объект имеет другие итераторы, отличные отeach?irb(main):001:0> "Hello".chars.select { |c| c =~ /[A-Z]/ } => ["H"] irb(main):002:0> "Hello".bytes.sort => [72, 101, 108, 108, 111]вызов итератора без блока возвращает перечислитель, а затем вы можете вызвать другие перечислимые методы на этом.
возвращаясь к волокнам, вы использовали элемент
takeметод с Перечислимым?class InfiniteSeries include Enumerable def each i = 0 loop { yield(i += 1) } end endесли что-нибудь вызывает это
eachметод, похоже, он никогда не должен возвращаться, верно? Проверьте это:InfiniteSeries.new.take(10) # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]я не знаю, если это использует волокна под капотом, но это может. Волокна могут использоваться для реализации бесконечных списков и ленивой оценки ряда. Для примера некоторых ленивых методов, определенных с помощью Перечислителей, я определил некоторые здесь: https://github.com/alexdowad/showcase/blob/master/ruby-core/collections.rb
вы можете также построить общецелевое средство coroutine используя волокна. Я никогда не использовал сопрограммы ни в одной из своих программ, но это хорошая концепция, чтобы знать.
я надеюсь, что это дает вам некоторое представление о возможностях. Как я уже говорил в начале, волокна являются примитивом управления потоком низкого уровня. Они делают его возможным поддерживать множественные "положения" контрол-подачи внутри ваше программируйте (как разные "закладки" на страницах книги) и переключайтесь между ними по желанию. Поскольку произвольный код может работать в волокне, вы можете вызвать сторонний код на волокне, а затем "заморозить" его и продолжить делать что-то еще, когда он вызывает обратно в код, который вы контролируете.
представьте себе что-то вроде этого: вы пишете серверную программу, которая будет обслуживать много клиентов. Полное взаимодействие с клиентом включает в себя выполнение ряда шагов, но каждое соединение является переходный, и вы должны помнить состояние для каждого клиента между соединениями. (Звучит как веб-программирование?)
вместо явного сохранения этого состояния и проверки его каждый раз, когда клиент подключается (чтобы увидеть, что следующий "шаг" они должны сделать), вы можете поддерживать волокно для каждого клиента. После идентификации клиента, вы бы получить их волокна и повторно запустить его. Затем в конце каждого соединения вы приостанавливаете волокно и сохраняете его снова. Таким образом, вы могли бы написать линейный код для реализации всей логики для полного взаимодействия, включая все шаги (так же, как вы, естественно, если бы ваша программа была запущена локально).
я уверен, что есть много причин, почему такая вещь может быть не удобно (по крайней мере сейчас), но опять же я просто пытаюсь показать вам некоторые возможности. Кто знает; как только вы получите концепцию, вы можете придумать какое-то совершенно новое приложение, о котором еще никто не думал!
В отличие от замыканий, которые имеют определенную точку входа и выхода, волокна могут сохранять свое состояние и возвращать (выход) много раз:
f = Fiber.new do puts 'some code' param = Fiber.yield 'return' # sent parameter, received parameter puts "received param: #{param}" Fiber.yield #nothing sent, nothing received puts 'etc' end puts f.resume f.resume 'param' f.resumeпечатает это:
some code return received param: param etcреализация этой логики с другими функциями ruby будет менее читаемой.
С этой особенностью, хорошее использование волокон сделать ручное кооперативное планирование (как замена потоков). У Ильи Григорика есть хороший пример того, как включить асинхронную библиотеку (
eventmachineв этом случае) в что выглядит как синхронный API без потери преимуществ IO-планирования асинхронного выполнения. Вот это ссылке.
Comments