Зачем нам волокна - PullRequest
       38

Зачем нам волокна

94 голосов
/ 29 января 2012

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

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 я не могу?

Ответы [ 2 ]

220 голосов
/ 08 февраля 2012

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

Вероятно, использование волокон # 1 в Ruby состоит в реализации Enumerator s, чтоосновной класс 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)>

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

Это не имеет отношения к волокнам, но позвольте мне упомянуть еще одну вещь, которую вы можете сделать с помощью перечислителей: они позволяют применять перечислимые методы высшего порядка к другим итераторам, кроме each.Подумайте об этом: обычно все методы Enumerable, включая map, select, include?, inject и т. Д., all работают с элементами, полученными с помощью 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]

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

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

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

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

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

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

21 голосов
/ 29 января 2012

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

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

...