Волокна - это то, что вы, вероятно, никогда не будете использовать непосредственно в коде уровня приложения. Это примитив управления потоком, который вы можете использовать для создания других абстракций, которые затем вы используете в коде более высокого уровня.
Вероятно, использование волокон №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
Enumerable объекты, и их 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
Вы также можете построить средство сопрограмм общего назначения, используя волокна. Я еще никогда не использовал сопрограммы ни в одной из своих программ, но это хорошая идея, которую нужно знать.
Надеюсь, это дает вам некоторое представление о возможностях. Как я сказал в начале, волокна представляют собой примитив управления потоком низкого уровня. Они позволяют поддерживать несколько «позиций» потока управления в вашей программе (например, разные «закладки» на страницах книги) и переключаться между ними по желанию. Поскольку произвольный код может выполняться в волокне, вы можете вызвать сторонний код в волокне, а затем «заморозить» его и продолжить делать что-то еще, когда он обратится в код, который вы контролируете.
Представьте себе что-то вроде этого: вы пишете серверную программу, которая будет обслуживать множество клиентов. Полное взаимодействие с клиентом включает в себя выполнение ряда шагов, но каждое соединение является временным, и вы должны помнить состояние каждого клиента между соединениями. (Похоже на веб-программирование?)
Вместо того, чтобы явно сохранять это состояние и проверять его каждый раз, когда клиент подключается (чтобы увидеть, какой следующий «шаг» им нужно сделать), вы можете поддерживать волокно для каждого клиента. После идентификации клиента вы получите его волокно и перезапустите его. Затем в конце каждого подключения вы должны подвешивать волокно и снова хранить его. Таким образом, вы можете написать прямолинейный код для реализации всей логики полного взаимодействия, включая все шаги (точно так же, как если бы ваша программа была запущена локально).
Я уверен, что есть много причин, по которым такая вещь может оказаться непрактичной (по крайней мере, на данный момент), но, опять же, я просто пытаюсь показать вам некоторые возможности. Кто знает; как только вы усвоите концепцию, вы можете придумать совершенно новое приложение, о котором еще никто не придумал!