Dlaczego wracają coroutines? [Zamknięte]

19

Większość prac przygotowawczych do korupcji miała miejsce w latach 60. / 70., a następnie zatrzymano na rzecz alternatyw (np. Wątków)

Czy jest jakaś podstawa do wznowienia zainteresowania koroutynami, które pojawiły się w Pythonie i innych językach?

użytkownik1787812
źródło
9
Nie jestem pewien, czy kiedykolwiek wyszli.
Blrfl,

Odpowiedzi:

26

Korpusy nigdy nie odeszły, tymczasem były w cieniu innych rzeczy. Niedawno zwiększone zainteresowanie programowaniem asynchronicznym, a zatem i korporacjami, wynika w dużej mierze z trzech czynników: większej akceptacji technik programowania funkcjonalnego, zestawów narzędzi o słabym wsparciu dla prawdziwej równoległości (JavaScript! Python!), A co najważniejsze: różnych kompromisów między wątkami a korporacjami. W niektórych przypadkach użycia coroutines są obiektywnie lepsze.

Jednym z największych paradygmatów programowania lat 80., 90. i dziś jest OOP. Jeśli spojrzymy na historię OOP, a konkretnie na rozwój języka Simula, zobaczymy, że klasy wyewoluowały z koroutyn. Simula była przeznaczona do symulacji układów z dyskretnymi zdarzeniami. Każdy element systemu był osobnym procesem, który byłby wykonywany w odpowiedzi na zdarzenia na czas jednego etapu symulacji, a następnie pozwalał innym procesom wykonywać swoją pracę. Podczas opracowywania Simula 67 wprowadzono koncepcję klasy. Teraz trwały stan rogówki jest przechowywany w elementach obiektu, a zdarzenia są wywoływane przez wywołanie metody. Aby uzyskać więcej informacji, przeczytaj artykuł Rozwój języków SIMULA autorstwa Nygaard i Dahl.

Więc w zabawny sposób od zawsze używaliśmy coroutines, nazywaliśmy je po prostu obiektami i programowaniem sterowanym zdarzeniami.

W odniesieniu do paralelizmu istnieją dwa rodzaje języków: te, które mają odpowiedni model pamięci, i te, które nie mają. Model pamięci omawia takie rzeczy, jak: „Jeśli piszę do zmiennej, a następnie czytam z tej zmiennej w innym wątku, czy widzę starą wartość, nową wartość, a może nieprawidłową wartość? Co oznaczają słowa „przed” i „po”? Które operacje mają gwarancję atomowości? ”

Stworzenie dobrego modelu pamięci jest trudne, więc tego wysiłku nigdy nie podjęto w przypadku większości nieokreślonych dynamicznych języków open-source zdefiniowanych w implementacji: Perl, JavaScript, Python, Ruby, PHP. Oczywiście wszystkie te języki ewoluowały daleko poza „skrypty”, dla których zostały stworzone. Cóż, niektóre z tych języków mają jakiś dokument modelu pamięci, ale te nie są wystarczające. Zamiast tego mamy hacki:

  • Perl można skompilować z obsługą wątków, ale każdy wątek zawiera osobny klon pełnego stanu interpretera, co powoduje, że wątki są zbyt drogie. Jedyną korzyścią jest to, że wspólne podejście „nic” pozwala uniknąć wyścigów danych i zmusza programistów do komunikowania się tylko poprzez kolejki / sygnały / IPC. Perl nie ma silnej historii do przetwarzania asynchronicznego.

  • JavaScript zawsze miał bogate wsparcie dla programowania funkcjonalnego, więc programiści ręcznie kodowali kontynuacje / wywołania zwrotne w swoich programach, w których potrzebowali operacji asynchronicznych. Na przykład z żądaniami Ajax lub opóźnieniami animacji. Ponieważ sieć jest z natury asynchroniczna, istnieje wiele asynchronicznych kodów JavaScript, a zarządzanie wszystkimi tymi wywołaniami zwrotnymi jest niezwykle bolesne. Dlatego widzimy wiele wysiłków, aby lepiej zorganizować te oddzwanianie (Obietnice) lub całkowicie je wyeliminować.

  • Python ma tę niefortunną funkcję o nazwie Global Interpreter Lock. Zasadniczo model pamięci Python to „Wszystkie efekty pojawiają się sekwencyjnie, ponieważ nie ma równoległości. Tylko jeden wątek będzie uruchamiał kod Pythona jednocześnie. ”Tak więc, chociaż Python ma wątki, są one tak samo potężne jak coroutines. [1] Python może kodować wiele korporacji za pomocą funkcji generatora za pomocą yield. Przy właściwym zastosowaniu może to uniknąć większości piekła zwrotnego znanego z JavaScript. Nowszy system asynchroniczny / oczekujący z Python 3.5 sprawia, że ​​asynchroniczne idiomy są wygodniejsze w Pythonie i integruje pętlę zdarzeń.

    [1]: Technicznie ograniczenia te dotyczą tylko CPython, implementacji referencyjnej Python. Inne implementacje, takie jak Jython, oferują rzeczywiste wątki, które mogą być wykonywane równolegle, ale muszą przejść wiele czasu, aby zaimplementować równoważne zachowanie. Zasadniczo: każda zmienna lub element obiektu jest zmienną lotną , dzięki czemu wszystkie zmiany są atomowe i są natychmiast widoczne we wszystkich wątkach. Oczywiście stosowanie zmiennych niestabilnych jest znacznie droższe niż stosowanie normalnych zmiennych.

  • Nie mam wystarczającej wiedzy na temat Ruby i PHP, aby poprawnie je upiec.

Podsumowując: niektóre z tych języków mają fundamentalne decyzje projektowe, które sprawiają, że wielowątkowość jest niepożądana lub niemożliwa, co prowadzi do większego skupienia się na alternatywach, takich jak coroutines i na sposobach uczynienia programowania asynchronicznego wygodniejszym.

Na koniec pomówmy o różnicach między coroutines a wątkami:

Wątki są w zasadzie procesami, z tym wyjątkiem, że wiele wątków w procesie współdzieli przestrzeń pamięci. Oznacza to, że nici nie są „lekkie” pod względem pamięci. Wątki są uprzednio planowane przez system operacyjny. Oznacza to, że przełączniki zadań mają duży narzut i mogą wystąpić w niewygodnych momentach. Narzut ten ma dwa składniki: koszt zawieszenia stanu wątku oraz koszt przełączania między trybem użytkownika (dla wątku) i trybem jądra (dla programu planującego).

Jeśli proces planuje swoje własne wątki bezpośrednio i kooperacyjnie, przełączanie kontekstu na tryb jądra nie jest konieczne, a przełączanie zadań jest porównywalnie kosztowne do pośredniego wywołania funkcji, jak w: dość tanie. Te lekkie nici mogą być nazywane zielonymi nitkami, włóknami lub koronami, w zależności od różnych szczegółów. Ważnymi użytkownikami zielonych nici / włókien były wczesne implementacje Java, a ostatnio Goroutines w Golang. Konceptualną zaletą coroutines jest to, że ich wykonanie można rozumieć w kategoriach przepływu kontroli wyraźnie przechodzącego w obie strony między coroutines. Jednak te korporacje nie osiągają prawdziwej równoległości, chyba że są zaplanowane w wielu wątkach systemu operacyjnego.

Gdzie przydatne są tanie kortyny? Większość oprogramowania nie potrzebuje nitek gazillionowych, więc normalne drogie nitki są zwykle OK. Jednak programowanie asynchroniczne może czasem uprościć kod. Aby móc swobodnie korzystać, ta abstrakcja musi być wystarczająco tania.

A potem jest sieć. Jak wspomniano powyżej, sieć jest z natury asynchroniczna. Żądania sieciowe po prostu zabierają dużo czasu. Wiele serwerów WWW utrzymuje pulę wątków pełną wątków roboczych. Jednak przez większość czasu wątki te pozostają na biegu jałowym, ponieważ czekają na jakiś zasób, czy to na zdarzenie we / wy podczas ładowania pliku z dysku, czekając, aż klient potwierdzi część odpowiedzi, czy czekając na bazę danych zapytanie zostało zakończone. NodeJS fenomenalnie wykazał, że konsekwentny i oparty na zdarzeniach asynchroniczny projekt serwera działa wyjątkowo dobrze. Oczywiście JavaScript jest daleki od jedynego języka używanego w aplikacjach internetowych, dlatego istnieje duża zachęta dla innych języków (zauważalnych w Pythonie i C #), aby ułatwić asynchroniczne programowanie w Internecie.

amon
źródło
Zalecam odszukanie czwartego do ostatniego akapitu, aby uniknąć ryzyka plagiatu, jest prawie dokładnie taki sam jak inne źródło, które przeczytałem. Ponadto, mimo że narzut jest o rząd wielkości mniejszy niż wątki, wydajności rogów nie można uprościć do „pośredniego wywołania funkcji”. Zobacz szczegóły ulepszeń na temat implementacji coroutine tutaj i tutaj .
kiedy
1
@snb Jeśli chodzi o sugerowaną edycję: GIL może być szczegółem implementacji CPython, ale podstawowym problemem jest to, że język Python nie ma jawnego modelu pamięci, który określa równoległą mutację danych. GIL to hack, aby ominąć te problemy. Jednak implementacje w języku Python z prawdziwą równoległością muszą przejść wiele starań, aby zapewnić równoważną semantykę, np. Jak omówiono w książce Jython . Zasadniczo: każda zmienna lub pole obiektu musi być kosztowną zmienną zmienną.
amon
3
@snb Odnośnie do plagiatu: Plagiat przedstawia fałszywie pomysły jako własne, szczególnie w kontekście akademickim. To poważny zarzut , ale jestem pewien, że nie miałeś tego na myśli. Paragraf „Wątki są w zasadzie procesami” jedynie przypomina dobrze znane fakty, o których mowa w każdym wykładzie lub podręczniku na temat systemów operacyjnych. Ponieważ istnieje tylko tyle sposobów zwięzłego sformułowania tych faktów, nie dziwię się, że akapit brzmi dobrze.
amon
I nie zmienia znaczenia sugerować, że Python nie mają modelu pamięci. Również użycie zmiennej lotnej samo w sobie nie zmniejsza wydajności niestabilności oznacza po prostu, że kompilator nie może zoptymalizować zmiennej w sposób, w jaki można założyć, że zmienna pozostanie niezmieniona bez jawnych operacji w bieżącym kontekście. W świecie Jython może to mieć znaczenie, ponieważ będzie on korzystał z kompilacji VM JIT, ale w świecie CPython nie martwisz się o optymalizację JIT, twoje zmienne zmienne istniałyby w przestrzeni wykonawczej interpretera, gdzie nie można dokonać optymalizacji .
kiedy
7

Coroutyny były przydatne, ponieważ systemy operacyjne nie przeprowadzały planowania wyprzedzającego . Gdy zaczęli zapewniać planowanie wyprzedzające, dłużej trzeba było okresowo rezygnować z kontroli w swoim programie.

W miarę, jak procesory wielordzeniowe stają się coraz bardziej powszechne, używa się coroutines do osiągnięcia równoległości zadań i / lub utrzymania wysokiego poziomu wykorzystania systemu (gdy jeden wątek wykonania musi czekać na zasobie, inny może zacząć działać w jego miejscu).

NodeJS to szczególny przypadek, w którym używane są coroutines, uzyskują równoległy dostęp do IO. Oznacza to, że do obsługi żądań We / Wy używa się wielu wątków, ale do wykonywania kodu javascript używany jest jeden wątek. Celem wykonania kodu użytkownika w wątku sygnalizacyjnym jest uniknięcie konieczności używania muteksów. To należy do kategorii prób utrzymania wysokiego poziomu wykorzystania systemu, jak wspomniano powyżej.

dlasalle
źródło
4
Ale coroutines nie są zarządzane przez system operacyjny. System operacyjny nie wie, co to jest coroutine, w przeciwieństwie do włókien C ++
przepełnienie
Wiele systemów operacyjnych ma coroutines.
Jörg W Mittag
coroutines, takie jak python i JavaScript ES6 +, nie są jednak wieloprocesowe? W jaki sposób osiągają one równoległość zadań?
kiedy
1
@Mael Ostatnie „odrodzenie” koronek pochodzi z Pythona i javascript, które, jak rozumiem, nie osiągają paralelności z ich koronami. To znaczy, że ta odpowiedź jest niepoprawna, ponieważ paralityk zadaniowy nie jest powodem, dla którego korupty w ogóle się „cofają”. Czy Luas też nie jest wieloprocesowy? EDYCJA: Właśnie zdałem sobie sprawę, że nie mówisz o równoległości, ale dlaczego przede wszystkim mi odpowiedziałeś? Odpowiedz dlasalle, ponieważ najwyraźniej się mylą w tej sprawie.
kiedy
3
@dlasalle Nie, nie mogą, pomimo faktu, że napis „działa równolegle”, co nie oznacza, że ​​żaden kod jest uruchamiany fizycznie w tym samym czasie. GIL to zatrzyma, a asynchronizacja nie odradza osobnych procesów wymaganych do wieloprocesowego przetwarzania w CPython (osobne GIL). Async działa z wydajnością dla pojedynczego wątku. Kiedy mówią „parralel”, w rzeczywistości mają na myśli kilka funkcji, które wywołują inne funkcje, oraz wykonywanie funkcji przeplatania . Procesów asynchronicznych w języku Python nie można uruchamiać równolegle z powodu impl. Mam teraz trzy języki, które nie używają parralelów, Lua, Javascript i Python.
kiedy
5

Wczesne systemy wykorzystywały coroutines do zapewnienia współbieżności przede wszystkim dlatego, że są najprostszym sposobem na zrobienie tego. Wątki wymagają sporego wsparcia ze strony systemu operacyjnego (możesz je wdrożyć na poziomie użytkownika, ale będziesz potrzebować jakiegoś sposobu, aby system okresowo przerywał proces) i jest trudniejszy do wdrożenia, nawet jeśli masz wsparcie .

Wątki zaczęły przejmować później, ponieważ w latach 70. i 80. obsługiwane były przez wszystkie poważne systemy operacyjne (a w latach 90. nawet Windows!) I są bardziej ogólne. I są łatwiejsze w użyciu. Nagle wszyscy myśleli, że wątki są kolejną wielką rzeczą.

Pod koniec lat 90. zaczęły pojawiać się pęknięcia, a na początku 2000 r. Stało się jasne, że istnieją poważne problemy z wątkami:

  1. zużywają dużo zasobów
  2. Przełączanie kontekstu zajmuje stosunkowo dużo czasu i często jest niepotrzebne
  3. niszczą lokalizację odniesienia
  4. pisanie poprawnego kodu koordynującego wiele zasobów, które mogą wymagać wyłącznego dostępu, jest nieoczekiwanie trudne

Z biegiem czasu liczba zadań, które zwykle muszą wykonywać programy w dowolnym momencie, szybko rośnie, zwiększając problemy spowodowane przez (1) i (2) powyżej. Różnica między szybkością procesora a czasem dostępu do pamięci rośnie, pogarszając problem (3). Rosnąca złożoność programów pod względem liczby i potrzebnych zasobów, co zwiększa znaczenie problemu (4).

Ale tracąc odrobinę ogólności i nakładając na programistę dodatkowy ciężar, aby zastanowić się, jak ich procesy mogą działać razem, coroutines mogą rozwiązać wszystkie te problemy.

  1. Coroutines wymagają niewiele więcej zasobów niż garść stron do układania, znacznie mniej niż większość implementacji wątków.
  2. Korpusy zmieniają kontekst tylko w punktach zdefiniowanych przez programistę, co, mam nadzieję, oznacza, że ​​tylko wtedy, gdy jest to konieczne. Zwykle nie muszą też zachowywać tyle informacji kontekstowych (np. Wartości rejestrów), ile robią wątki, co oznacza, że ​​każdy przełącznik jest zwykle szybszy i potrzebuje ich mniej.
  3. Wspólne wzorce reklam, w tym operacje typu producent / konsument, przekazują dane między procedurami w sposób, który aktywnie zwiększa lokalność. Ponadto przełączanie kontekstu zwykle występuje tylko między jednostkami pracy, które się w nich nie znajdują, tj. W czasie, gdy i tak zwykle minimalizuje się lokalizację.
  4. Blokowanie zasobów jest mniej prawdopodobne, gdy procedury wiedzą, że nie można ich arbitralnie przerwać w trakcie operacji, umożliwiając prostsze implementacje do poprawnego działania.
Jules
źródło
5

Przedmowa

Chciałbym zacząć od podania przyczyny, dla której corutyny nie dostają odrodzenia, paralelizmu. Ogólnie rzecz biorąc, nowoczesne korporacje nie są sposobem na osiągnięcie równoległości opartej na zadaniach, ponieważ nowoczesne implementacje nie wykorzystują funkcji wieloprocesowej. Najbliższe rzeczy, które można uzyskać, to takie jak włókna .

Nowoczesne wykorzystanie (dlaczego wróciły)

Nowoczesne coroutines stały się sposobem na leniwą ewaluację , co jest bardzo przydatne w funkcjonalnych językach, takich jak haskell, w których zamiast iteracji całego zestawu w celu wykonania operacji, byłbyś w stanie wykonać tylko taką ocenę, ile potrzeba ( przydatne w przypadku nieskończonych zestawów przedmiotów lub innych dużych zestawów z wcześniejszym zakończeniem i podzbiorami).

Dzięki użyciu słowa kluczowego Yield do tworzenia generatorów (które same w sobie zaspokajają część leniwych potrzeb ewaluacyjnych) w językach takich jak Python i C #, współczesne implementacje były nie tylko możliwe, ale możliwe bez specjalnej składni w samym języku (chociaż python ostatecznie dodał kilka bitów, aby pomóc). Co-rutyny pomoc w leniwe evaulation z ideą przyszłość s gdzie jeśli nie trzeba wartość zmiennej w tym czasie, można opóźnić faktycznie pozyskania go, dopóki jawnie poprosić o tej wartości (co pozwala na użycie wartości i leniwie oceniaj to w innym czasie niż tworzenie instancji).

Jednak poza leniwą oceną, szczególnie w sferze internetowej, te wspólne procedury pomagają naprawić piekło wywołania zwrotnego . Coroutyny stają się przydatne w dostępie do bazy danych, transakcji online, interfejsu użytkownika itp., W których czas przetwarzania na komputerze klienta nie spowoduje szybszego dostępu do potrzebnych informacji. Wątkowanie może wypełnić to samo, ale wymaga o wiele więcej narzutów w tej sferze, w przeciwieństwie do coroutines, w rzeczywistości są przydatne do równoległości zadań .

Krótko mówiąc, wraz z rozwojem tworzenia stron internetowych i paradygmatów funkcjonalnych coraz bardziej łączą się z imperatywnymi językami, coroutines stały się rozwiązaniem problemów asynchronicznych i leniwej oceny. Korpusy pojawiają się w przestrzeniach problemowych, w których wielowątkowe gwintowanie i gwintowanie są albo niepotrzebne, niewygodne, albo niemożliwe.

Nowoczesny przykład

Wszystkie języki w językach takich jak Javascript, Lua, C # i Python czerpią swoje implementacje z poszczególnych funkcji, rezygnując z kontroli głównego wątku nad innymi funkcjami (nie ma to nic wspólnego z wywołaniami systemu operacyjnego).

W tym przykładzie Pythona mamy zabawną funkcję Pythona z czymś zwanym awaitwewnątrz niego. Zasadniczo jest to wydajność, która daje wykonanie, loopktóra następnie umożliwia uruchomienie innej funkcji (w tym przypadku innej factorialfunkcji). Zauważ, że gdy mówi „Równoległe wykonywanie zadań”, co jest mylące, tak naprawdę nie jest wykonywane równolegle, a jego funkcja przeplatania jest wykonywana za pomocą słowa kluczowego „ czekaj” (należy pamiętać, że jest to specjalny rodzaj wydajności)

Pozwalają one na uzyskanie pojedynczych, nierównoległych wydajności sterowania dla współbieżnych procesów, które nie są równoległe do zadań , w tym sensie, że zadania te nie działają nigdy jednocześnie. Korpusy nie są wątkami we współczesnych implementacjach językowych. Wszystkie implementacje tych języków w tych procedurach wywodzą się z wywołań funkcji, które programista musi ręcznie wprowadzić do swoich procedur.

EDYCJA: C ++ Boost coroutine2 działa w ten sam sposób, a ich wyjaśnienie powinno dać lepszy obraz tego, o czym mówię z dziećmi, patrz tutaj . Jak widać, nie ma „specjalnego przypadku” z implementacjami, takie jak włókna wzmacniające są wyjątkiem od reguły, a nawet wtedy wymagają wyraźnej synchronizacji.

EDYCJA 2: ponieważ ktoś myślał, że mówię o systemie opartym na zadaniach c #, nie byłem. Mówiłem o systemie Unity i naiwnych implementacjach c #

whn
źródło
@ T.Sar Nigdy nie powiedziałem, że C # ma jakieś „naturalne” korupcje, ani C ++ (może się zmienić), ani Python (i nadal je miał), a wszystkie trzy mają wspólne implementacje. Ale wszystkie implementacje korupcji w C # (jak te w jedności) oparte są na wydajności, jak to opisuję. Także twoje użycie „hacka” tutaj jest bez znaczenia, myślę, że każdy program to hack, ponieważ nie zawsze był zdefiniowany w języku. W żaden sposób nie mieszam „systemu zadań” w języku C # z niczym, nawet o tym nie wspominałem.
kiedy
Sugeruję, aby twoja odpowiedź była nieco jaśniejsza. C # ma zarówno koncepcję oczekiwania na instrukcje, jak i oparty na zadaniach system paralelizmu - użycie C # i tych słów, podając przykłady w pythonie o tym, że python nie jest tak naprawdę równoległy, może powodować wiele nieporozumień. Usuń także pierwsze zdanie - niepotrzebne jest bezpośrednie atakowanie innych użytkowników w takiej odpowiedzi.
T. Sar - Przywróć Monikę