Зачем нужны волокна

101

Для волокон у нас есть классический пример: генерация чисел Фибоначчи.

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 }

вернет точно такой же результат.

Итак, в чем преимущества волокон. Какие вещи я могу писать с помощью Fibers, чего не могу делать с лямбдами и другими классными функциями Ruby?

fl00r
источник
4
Старый пример Фибоначчи - это наихудший из возможных мотиваторов ;-) Есть даже формула, которую вы можете использовать для вычисления любого числа Фибоначчи в O (1).
usr
17
Проблема не в алгоритме, а в понимании волокон :)
fl00r

Ответы:

230

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

Вероятно, использование волокон №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)>

Это EnumeratorEnumerable объекты, и их eachметоды выдают элементы, которые были бы выданы исходным методом итератора, если бы он был вызван с блоком. В примере, который я только что привел, возвращаемый Enumerator reverse_eachимеет eachметод, который дает 3,2,1. Перечислитель, возвращаемый функцией, charsдает "c", "b", "a" (и так далее). НО, в отличие от исходного метода итератора, Enumerator также может возвращать элементы один за другим, если вы вызываете 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как прямой код, но Enumerator может запускать их по одному . В промежутках между вызовами nextвыполнение an_iterator"замораживается". Каждый раз, когда вы звоните next, он продолжает работать до следующего yieldоператора, а затем снова «зависает».

Угадаете, как это реализовано? Enumerator завершает вызов an_iteratorв волокне и передает блок, который приостанавливает волокно . Таким образом, каждый раз, когда an_iteratorуступает место блоку, волокно, на котором он работает, приостанавливается, и выполнение продолжается в основном потоке. В следующий раз, когда вы вызовете next, он передает управление волокну, блок возвращается иan_iterator продолжает работу с того места, где он остановился.

Было бы поучительно подумать, что для этого потребуется без волокон. КАЖДЫЙ класс, который хотел предоставить как внутренние, так и внешние итераторы, должен был бы содержать явный код для отслеживания состояния между вызовами next. Каждый вызов next должен будет проверять это состояние и обновлять его перед возвратом значения. С помощью фибров мы можем автоматически преобразовать любой внутренний итератор во внешний.

Это не имеет отношения к persay-волокнам, но позвольте мне упомянуть еще об одной вещи, которую вы можете делать с помощью Enumerators: они позволяют вам применять методы Enumerable более высокого порядка к другим итераторам, кроме 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]

Вызов итератора без блока возвращает Enumerator, а затем вы можете вызывать для него другие методы Enumerable.

Возвращаясь к волокнам, вы использовали takeметод из Enumerable?

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]

Я не знаю, используются ли волокна под капотом, но может. Волокна могут использоваться для реализации бесконечных списков и ленивого вычисления ряда. В качестве примера некоторых ленивых методов, определенных с помощью Enumerators, я определил некоторые здесь: https://github.com/alexdowad/showcase/blob/master/ruby-core/collections.rb

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

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

Представьте себе что-то вроде этого: вы пишете серверную программу, которая будет обслуживать множество клиентов. Полное взаимодействие с клиентом включает в себя выполнение ряда шагов, но каждое соединение является временным, и вы должны помнить состояние каждого клиента между соединениями. (Похоже на веб-программирование?)

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

Я уверен, что есть много причин, по которым такая вещь может оказаться непрактичной (по крайней мере, на данный момент), но, опять же, я просто пытаюсь показать вам некоторые возможности. Кто знает; как только вы усвоите концепцию, вы можете придумать совершенно новое приложение, о котором еще никто не придумал!

Alex D
источник
Спасибо за ответ! Так почему же они не реализуют charsдругие перечислители с простыми замыканиями?
fl00r
@ fl00r, я подумываю добавить еще больше информации, но не знаю, слишком ли длинный этот ответ ... хотите еще?
Alex D
13
Мне кажется, этот ответ настолько хорош, что его следует написать где-нибудь в блоге.
Джейсон Фогеле
1
ОБНОВЛЕНИЕ: похоже, Enumerableв Ruby 2.0 будут включены некоторые «ленивые» методы.
Alex D
2
takeне требует волокна. Вместо этого takeпросто ломается во время n-го выхода. При использовании внутри блока breakвозвращает управление фрейму, определяющему блок. a = [] ; InfiniteSeries.new.each { |x| a << x ; break if a.length == 10 } ; a
Мэтью
22

В отличие от замыканий, которые имеют определенную точку входа и выхода, волокна могут сохранять свое состояние и возвращать (выходить) много раз:

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-планирования асинхронного выполнения. Вот ссылка .

Алексей Ключников
источник
Спасибо! Я читаю документы, поэтому понимаю всю эту магию со множеством входов и выходов внутри фибры. Но я не уверен, что это облегчает жизнь. Я не думаю, что пытаться следить за всеми этими резюме и доходами - хорошая идея. Похоже на клубок, который сложно распутать. Поэтому я хочу понять, есть ли случаи, когда этот клубок волокон является хорошим решением. Eventmachine - это круто, но не лучшее место для понимания волокон, потому что сначала вы должны разобраться во всех этих схемах реактора. Так что я верю, что могу понять волокна physical meaningна более простом примере
fl00r