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

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

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

So

10.times { puts fib.resume }

и

prc = clsr 
10.times { puts prc.call }

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

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

Ответ 1

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

Вероятно, использование # 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" (и так далее). НО, в отличие от исходного метода итератора, 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"

Возможно, вы слышали о "внутренних итераторах" и "внешних итераторах" (хорошее описание обоих приведено в книге "Шаблоны моделей" Банда четырех "). В приведенном выше примере показано, что Enumerators могут использоваться для превращения внутреннего итератора во внешний.

Это один из способов сделать ваши собственные счетчики:

class SomeClass
  def an_iterator
    # note the 'return enum_for...' pattern; it 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, а затем снова "зависает".

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

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

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

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

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

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

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

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

Ответ 2

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

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