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ł!
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
Problem nie dotyczy algorytmu, ale zrozumienia włókien :) – fl00r