2012-01-29 21 views
80

dla włókien mamy klasyczny przykład: generowanie liczb FibonacciegoDlaczego potrzebujemy włókna

fib = Fiber.new do 
    x, y = 0, 1 
    loop do 
    Fiber.yield y 
    x,y = y,x+y 
    end 
end 

Dlaczego potrzebujemy Włókna tutaj? Mogę przepisać to tylko z samym Proc (zamknięcie, faktycznie)

def clsr 
    x, y = 0, 1 
    Proc.new do 
    x, y = y, x + y 
    x 
    end 
end 

Więc

10.times { puts fib.resume } 

i

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

powróci tylko sam wynik.

Jakie są zalety włókien. Jakiego rodzaju rzeczy mogę pisać z Fibres, których nie mogę zrobić z lambdami i innymi ciekawymi funkcjami Ruby?

+4

Stary przykład Fibonacciego jest najgorszym możliwym czynnikiem motywującym ;-) Istnieje nawet wzór, za pomocą którego można obliczyć liczbę _any_ fibonacci w O (1). – usr

+15

Problem nie dotyczy algorytmu, ale zrozumienia włókien :) – fl00r

Odpowiedz

197

Włókna to coś, czego prawdopodobnie nigdy nie użyjesz bezpośrednio w kodzie poziomu aplikacji. Są prymitywem kontroli przepływu, który można wykorzystać do budowania innych abstrakcji, które następnie wykorzystuje się w kodzie wyższego poziomu.

Prawdopodobnie pierwszym użyciem włókien w Ruby jest implementacja Enumerator s, które są rdzenną klasą Ruby w Rubim 1.9. Są to użyteczne niewiarygodnie.

W Rubim 1.9, jeśli wywołasz prawie każdą metodę iteratora w klasach podstawowych, bez przekazując blok, zwróci on 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)> 

te Enumerator S są przeliczalnego obiektów i ich each metody dają się elementów, które zostały uzyskane w oryginalnej metodzie iteracyjnej, gdyby został nazwany z blokiem. W podanym przeze mnie przykładzie Enumerator zwrócony przez reverse_each ma metodę each, która daje 3,2,1. Enumerator zwrócony przez chars daje "c", "b", "a" (i tak dalej). Jednak w odróżnieniu od oryginalnej metodzie iteracyjnej, moduł wyliczający można również zwrócić jedną elementów o jeden, jeśli zadzwonisz next na nim wielokrotnie:

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" 

może słyszeliście o „iteratorów wewnętrznych” i „iteratorów zewnętrznych” (dobry opis obu znajduje się w książce "Gang of Four" Design Patterns). Powyższy przykład pokazuje, że Enumeratory mogą zostać użyte do przekształcenia wewnętrznego iteratora w zewnętrzny.

Jest jeden sposób, aby tworzyć własne rachmistrzów:

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 

Spróbujmy go:

e = SomeClass.new.an_iterator 
e.next # => 1 
e.next # => 2 
e.next # => 3 

Chwileczkę ... Czy cokolwiek dziwne nie? Napisałeś yield w an_iterator jako kod linii prostej, ale Enumerator może je uruchomić po jednym na raz. Pomiędzy wywołaniami next, wykonanie an_iterator jest "zamrożone". Za każdym razem, gdy wywołujesz next, kontynuuje on do następnego oświadczenia yield, a następnie "zamraa" ponownie.

Czy możesz zgadnąć, w jaki sposób jest to realizowane? Enumerator zawija połączenie do an_iterator w światłowodzie i przekazuje blok, który zawiesza światłowód. Za każdym razem, gdy an_iterator przechodzi do bloku, włókno, na którym działa, jest zawieszone, a wykonanie kontynuowane jest na głównym wątku. Następnym razem, gdy zadzwonisz pod numer next, przekaże kontrolę do włókna, , blok zwróci, a an_iterator będzie kontynuował od miejsca, w którym zostało przerwane.

To byłoby pouczające, aby myśleć o tym, co byłoby wymagane, aby to zrobić bez włókien. KAŻDE klasy, które chciałyby dostarczać zarówno wewnętrzne, jak i zewnętrzne iteratory, musiałyby zawierać wyraźny kod do śledzenia stanu między połączeniami do next. Każde wywołanie do następnego będzie musiało sprawdzić ten stan i zaktualizować go przed zwróceniem wartości. W przypadku włókien możemy automatycznie przekonwertować dowolny wewnętrzny iterator na zewnętrzny.

To nie ma nic wspólnego z włóknami, ale pozwólcie, że wspomnę o jeszcze jednej rzeczy, którą można zrobić z Enumeratorami: umożliwiają one zastosowanie metod wyliczalnych wyższego rzędu do innych iteratorów innych niż each. Pomyśl o tym: normalnie wszystkie metody podlegające liczniejszej liczbie, w tym , select, include?,i wszystkie, są przetwarzane na elementach przedstawionych przez each. Ale co jeśli obiekt ma inne iteratory inne niż 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] 

Wywołanie iterator bez bloku zwraca Enumerator, a następnie można wywołać inne przeliczalny metod na to.

Wracając do włókien, skorzystałeś z metody take z Enumerable?

class InfiniteSeries 
    include Enumerable 
    def each 
    i = 0 
    loop { yield(i += 1) } 
    end 
end 

Jeśli coś nazywa że each sposób, wygląda na to, że nigdy nie powinien wrócić, prawda? Sprawdź to:

InfiniteSeries.new.take(10) # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 

Nie wiem, czy ta wykorzystuje włókna pod maską, ale to dało. Włókna mogą być używane do implementacji nieskończonych list i leniwej oceny serii. Na przykład niektórych leniwych metod zdefiniowanych przy użyciu enumeratorów, zdefiniowałem kilka tutaj: https://github.com/alexdowad/showcase/blob/master/ruby-core/collections.rb

Można również zbudować obiekt koroutynowy ogólnego przeznaczenia z wykorzystaniem włókien. Nigdy nie używałem coroutines w żadnym z moich programów jeszcze, ale to dobry pomysł, aby wiedzieć.

Mam nadzieję, że daje to pewne wyobrażenie o możliwościach. Jak już powiedziałem na początku, włókna są prymitywem niskiego poziomu kontroli przepływu. Pozwalają one zachować wiele pozycji "sterowania" przepływem w twoim programie (jak różne "zakładki" na stronach książki) i przełączać się między nimi według potrzeb. Ponieważ dowolny kod może działać w światłowodzie, możesz wywołać kod zewnętrzny na światłowodzie, a następnie "zamrozić" go i kontynuować wykonywanie innej czynności, gdy wróci do kodu, który kontrolujesz.

Wyobraź sobie coś takiego: piszesz program serwera, który obsługuje wielu klientów. Pełna interakcja z klientem wymaga wykonania serii kroków, ale każde połączenie jest przejściowe i musisz pamiętać stan dla każdego klienta między połączeniami. (Brzmi jak programowanie sieciowe?)

Zamiast jawnie zapisywać ten stan i sprawdzać go za każdym razem, gdy klient się połączy (aby zobaczyć, co musi zrobić następny "krok"), można utrzymywać światłowód dla każdego klienta . Po zidentyfikowaniu klienta można pobrać jego światłowód i ponownie go uruchomić. Następnie, na końcu każdego połączenia, zawiesisz włókno i zapiszesz je ponownie. W ten sposób możesz napisać kod w linii prostej, aby wdrożyć całą logikę dla pełnej interakcji, w tym wszystkie kroki (tak jak naturalnie, gdyby program został uruchomiony na miejscu).

Jestem pewien, że jest wiele powodów, dla których takie rzeczy mogą nie być praktyczne (przynajmniej na razie), ale znowu próbuję pokazać ci niektóre z możliwości. Kto wie; gdy pojawi się pomysł, możesz wymyślić zupełnie nową aplikację, o której nikt jeszcze nie pomyślał!

+0

Dziękuję za odpowiedź! Dlaczego więc nie implementują 'chars' lub innych modułów wyliczających z tylko zamknięciami? – fl00r

+0

@ fl00r, Zastanawiam się nad dodaniem jeszcze większej ilości informacji, ale nie wiem, czy ta odpowiedź jest już zbyt długa ... czy chcesz więcej? –

+0

Chcę! :) Z wielką przyjemnością! – fl00r

17

przeciwieństwie a zamknięć, które mają określony wejścia i wyjścia włókna może zachować swój stan i zwrotny (wydajności) wiele razy:

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 

drukuje:

some code 
return 
received param: param 
etc 

Realizacja tego logika z innymi funkcjami ruby ​​będzie mniej czytelna.

Dzięki tej funkcji, dobrym stosowaniem włókien jest ręczne planowanie współpracy (jako wymiana gwintów). Ilya Grigorik ma dobry przykład, jak włączyć asynchroniczną bibliotekę (eventmachine w tym przypadku) w coś, co wygląda jak synchroniczny interfejs API bez utraty zalet planowania IO asynchronicznego wykonywania. Oto link.

+0

Dziękujemy! Czytam dokumentację, więc rozumiem całą tę magię z wieloma wejściami i wyjściami we włóknie. Ale nie jestem pewien, czy te rzeczy ułatwiają życie. Nie sądzę, że dobrze jest próbować podążać za tymi wszystkimi życiorysami i zyskami. Wygląda jak szot, który trudno rozplątać. Chcę więc zrozumieć, czy są przypadki, w których ta nić włókien jest dobrym rozwiązaniem. Eventmachine jest fajne, ale nie jest najlepszym miejscem do zrozumienia włókien, ponieważ najpierw powinieneś zrozumieć wszystkie te rzeczy z reaktora. Wierzę, że rozumiem fizyczne znaczenie włókien w prostszym przykładzie. – fl00r