Для чого потрібні волокна


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 }

поверне такий самий результат.

То які переваги волокон. Які речі я можу писати за допомогою Волокна, яку я не можу робити з лямбдами та іншими цікавими функціями Ruby?


4
Старий приклад філософії - це лише найгірший можливий мотиватор ;-) Існує навіть формула, яку ви можете використовувати для обчислення будь-якого числа поле в O (1).
usr

17
Проблема не в алгоритмі, а в розумінні волокон :)
fl00r

Відповіді:


230

Волокна - це те, що ви, ймовірно, ніколи не використовуєте безпосередньо в коді на рівні додатків. Вони є примітивом управління потоком, який ви можете використовувати для створення інших абстракцій, які потім використовуєте у коді вищого рівня.

Ймовірно, використання №1 волокон у Ruby - це реалізація Enumerators, які є основним класом Ruby в Ruby 1.9. Це неймовірно корисні.

У Ruby 1.9, якщо ви викликаєте майже будь-який метод ітератора на основних класах, не передаючи блок, він поверне an 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врожаях "с", "б", "а" (і так далі). АЛЕ, на відміну від оригінального методу ітератора, Перелік також може повертати елементи один за одним, якщо ви звертаєтесь 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. Кожен дзвінок до наступного повинен був перевірити цей стан та оновити його, перш ніж повертати значення. За допомогою волокон ми можемо автоматично перетворити будь-який внутрішній ітератор у зовнішній.

Це не має відношення до волокон, можливо, але дозвольте зазначити ще одну річ, яку ви можете зробити з Enumerators: вони дозволяють застосовувати численні методи вищого порядку до інших ітераторів, крім 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метод від 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

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

Я сподіваюся, що це дає вам деяке уявлення про можливості. Як я вже говорив на початку, волокна є примітивом низького рівня контролю потоку. Вони дозволяють підтримувати декілька "позицій" у вашій програмі (наприклад, різні "закладки" на сторінках книги) та перемикатися між ними за бажанням. Оскільки довільний код може працювати у волокні, ви можете зателефонувати в сторонній код на волокні, а потім "заморозити" його та продовжувати робити щось інше, коли він передзвонить у керований вами код.

Уявіть щось подібне: ви пишете серверну програму, яка обслуговуватиме багатьох клієнтів. Повна взаємодія з клієнтом передбачає проходження низки кроків, але кожне з'єднання є тимчасовим, і ви повинні пам'ятати стан кожного клієнта між з'єднаннями. (Це схоже на веб-програмування?)

Замість того, щоб явно зберігати цей стан і перевіряти його щоразу, коли клієнт підключається (щоб побачити, що наступний "крок", який він повинен зробити), ви можете підтримувати волокно для кожного клієнта. Визначившись із клієнтом, ви отримаєте його волокно та перезапустите його. Тоді в кінці кожного з'єднання ви призупиняєте волокно і зберігаєте його знову. Таким чином, ви можете написати прямолінійний код, щоб реалізувати всю логіку для повної взаємодії, включаючи всі кроки (так само, як ви, природно, якби ваша програма була запущена локально).

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


Дякую за вашу відповідь! То чому вони не реалізують charsчи інші перелічувачі з просто закриттями?
fl00r

@ fl00r, я думаю додати ще більше інформації, але не знаю, чи ця відповідь вже занадто довга ... ви хочете більше?
Алекс Д

13
Ця відповідь настільки хороша, що її слід десь писати як допис у блозі, міркуючи.
Джейсон Вогеле

1
ОНОВЛЕННЯ: Схоже, Enumerableбудуть включені деякі "ледачі" методи в Ruby 2.0.
Алекс Д

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

Реалізація цієї логіки з іншими рубіновими функціями буде менш читабельною.

Завдяки цій особливості, для хорошого використання волокон слід робити ручне спільне планування (як заміна ниток). У Іллі Григорика є хороший приклад того, як перетворити асинхронну бібліотеку ( eventmachineв даному випадку) на те, що схоже на синхронний API, не втрачаючи переваг IO-планування асинхронного виконання. Ось посилання .


Дякую! Я читаю документи, тому розумію всю цю магію з багатьма записами та виходами всередині клітковини. Але я не впевнений, що цей матеріал полегшує життя. Я не думаю, що це гарна ідея намагатися слідувати всім цим резюме та результатам. Це схоже на зуб, який важко розплутати. Тому я хочу зрозуміти, чи є випадки, коли ця зашморг волокон є хорошим рішенням. Eventmachine - це круто, але не найкраще місце для розуміння волокон, тому що спочатку слід зрозуміти все, що стосується реактора. Тому я вважаю, що я можу зрозуміти волокна physical meaningна більш простому прикладі
fl00r
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.