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?
python
multithreading
concurrency
multitasking
użytkownik1787812
źródło
źródło
Odpowiedzi:
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.
źródło
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.
źródło
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:
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.
źródło
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
await
wewnątrz niego. Zasadniczo jest to wydajność, która daje wykonanie,loop
która następnie umożliwia uruchomienie innej funkcji (w tym przypadku innejfactorial
funkcji). 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 #
źródło