Włókna to coś, czego prawdopodobnie nigdy nie użyjesz bezpośrednio w kodzie na poziomie aplikacji. Są to prymityw do sterowania przepływem, którego można użyć do tworzenia innych abstrakcji, których można następnie używać w kodzie wyższego poziomu.
Prawdopodobnie pierwszym zastosowaniem włókien w Rubim jest implementacja Enumerator
s, które są podstawową klasą Rubiego w Rubim 1.9. Są niezwykle przydatne.
W Rubim 1.9, jeśli wywołasz prawie każdą metodę iteratora na klasach rdzenia, bez przekazywania bloku, zwróci to plik 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)>
Są Enumerator
to obiekty Enumerable, a ich each
metody dają elementy, które zostałyby uzyskane przez oryginalną metodę iteratora, gdyby została wywołana z blokiem. W podanym przeze mnie przykładzie Enumerator zwrócony przez reverse_each
ma each
metodę, która zwraca 3,2,1. Enumerator zwrócony przez chars
zwraca „c”, „b”, „a” (i tak dalej). ALE, w przeciwieństwie do oryginalnej metody iteratora, Enumerator może również zwracać elementy jeden po drugim, jeśli wywołujesz next
go 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"
Być może słyszałeś o „wewnętrznych iteratorach” i „zewnętrznych iteratorach” (dobry opis obu znajduje się w książce „Gang of Four” Design Patterns). Powyższy przykład pokazuje, że Enumerators mogą służyć do przekształcania wewnętrznego iteratora w zewnętrzny.
Oto jeden ze sposobów tworzenia własnych modułów wyliczających:
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:
e = SomeClass.new.an_iterator
e.next # => 1
e.next # => 2
e.next # => 3
Chwileczkę ... czy coś tam wydaje się dziwne? Napisałeś yield
instrukcje an_iterator
jako kod liniowy, ale moduł wyliczający może uruchamiać je pojedynczo . W międzyczasie next
wykonanie polecenia an_iterator
jest „zawieszane”. Za każdym razem, gdy dzwonisz next
, przechodzi do następnej yield
instrukcji, a następnie ponownie „zawiesza się”.
Czy możesz zgadnąć, jak to jest realizowane? Enumerator zawija wywołanie do an_iterator
światłowodu i przekazuje blok, który zawiesza światłowód . Tak więc za każdym razem, gdy an_iterator
ustępuje blokowi, światłowód, na którym działa, jest zawieszany, a wykonywanie jest kontynuowane w głównym wątku. Następnym razem, gdy dzwonisz next
, przekazuje sterowanie do światłowodu, blok wraca i an_iterator
kontynuuje od miejsca, w którym został przerwany.
Pouczające byłoby zastanowienie się, co byłoby potrzebne do zrobienia tego bez włókien. KAŻDA klasa, która chciała udostępniać zarówno wewnętrzne, jak i zewnętrzne iteratory, musiałaby zawierać jawny kod, aby śledzić stan między wywołaniami next
. Każde wywołanie next musiałoby sprawdzić ten stan i zaktualizować go przed zwróceniem wartości. Dzięki światłowodom możemy automatycznie przekształcić dowolny wewnętrzny iterator na zewnętrzny.
Nie ma to nic wspólnego z włóknami, ale wspomnę jeszcze o jednej rzeczy, którą możesz zrobić z Enumeratorami: pozwalają one na zastosowanie metod Enumerable wyższego rzędu do innych iteratorów innych niż each
. Pomyśl o tym: normalnie wszystkie przeliczalne metody, w tym map
, select
, include?
, inject
, i tak dalej, wszystkie prace na elementach uzyskano 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 iteratora bez bloku zwraca Enumerator, a następnie możesz wywołać inne metody Enumerable.
Wracając do włókien, czy użyłeś take
metody z Enumerable?
class InfiniteSeries
include Enumerable
def each
i = 0
loop { yield(i += 1) }
end
end
Jeśli cokolwiek wywołuje tę each
metodę, wygląda na to, że nigdy nie powinna powrócić, prawda? Sprawdź to:
InfiniteSeries.new.take(10) # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Nie wiem, czy to wykorzystuje włókna pod maską, ale mogłoby. Włókna mogą służyć do implementacji nieskończonych list i leniwej oceny serii. Na przykład niektóre leniwe metody zdefiniowane w Enumerators, zdefiniowałem tutaj: https://github.com/alexdowad/showcase/blob/master/ruby-core/collections.rb
Możesz również zbudować ośrodek ogólnego przeznaczenia z użyciem włókien. Nigdy jeszcze nie używałem programów w żadnym z moich programów, ale warto wiedzieć.
Mam nadzieję, że to daje ci wyobrażenie o możliwościach. Jak powiedziałem na początku, włókna są prymitywem kontroli przepływu niskiego poziomu. Umożliwiają one utrzymywanie wielu „pozycji” przepływu sterowania w programie (jak różne „zakładki” na stronach książki) i przełączanie się między nimi w razie potrzeby. Ponieważ dowolny kod może działać w światłowodzie, możesz wywołać kod strony trzeciej na światłowodzie, a następnie „zamrozić” go i kontynuować wykonywanie innych czynności, gdy wywoła kod, który kontrolujesz.
Wyobraź sobie coś takiego: piszesz program serwera, który będzie obsługiwał wielu klientów. Pełna interakcja z klientem wymaga wykonania szeregu kroków, ale każde połączenie jest przejściowe i należy pamiętać stan każdego klienta między połączeniami. (Brzmi jak programowanie internetowe?)
Zamiast jawnie zapisywać ten stan i sprawdzać go za każdym razem, gdy klient się łączy (aby zobaczyć, jaki będzie następny „krok”, jaki musi wykonać), można zachować światłowód dla każdego klienta. Po zidentyfikowaniu klienta należy odzyskać jego włókno i ponownie go uruchomić. Następnie na końcu każdego połączenia zawieszasz światłowód i przechowujesz go ponownie. W ten sposób możesz napisać kod w linii prostej, aby zaimplementować całą logikę dla pełnej interakcji, w tym wszystkie kroki (tak jak naturalnie byś zrobił, gdyby twój program był uruchamiany lokalnie).
Jestem pewien, że istnieje wiele powodów, dla których taka rzecz może nie być praktyczna (przynajmniej na razie), ale znowu próbuję tylko pokazać niektóre możliwości. Kto wie; kiedy już zdobędziesz koncepcję, możesz wymyślić zupełnie nową aplikację, o której nikt inny jeszcze nie pomyślał!