Mam pytanie, chodzi o to, dlaczego programiści wydają się lubić współbieżność i programy wielowątkowe w ogóle.
Rozważam tutaj 2 główne podejścia:
- podejście asynchroniczne oparte w zasadzie na sygnałach lub po prostu podejście asynchroniczne wywoływane przez wiele dokumentów i języków, takich jak na przykład nowy C # 5.0, oraz „wątek towarzyszący”, który zarządza polityką Twojego potoku
- podejście równoległe lub podejście wielowątkowe
Powiem tylko, że myślę o sprzęcie tutaj i najgorszym scenariuszu, i sam przetestowałem te dwa paradygmaty, paradygmat asynchroniczny jest zwycięzcą w momencie, gdy nie rozumiem, dlaczego ludzie w 90% przypadków mówią o wielowątkowości, gdy chcą przyspieszyć lub dobrze wykorzystać swoje zasoby.
Testowałem programy wielowątkowe i program asynchroniczny na starym komputerze z czterordzeniowym procesorem Intel, który nie oferuje kontrolera pamięci wewnątrz procesora, pamięcią zarządza całkowicie płyta główna, więc w tym przypadku wydajność jest okropna z aplikacja wielowątkowa, nawet stosunkowo niewielka liczba wątków, takich jak 3-4-5, może stanowić problem, aplikacja nie reaguje i jest po prostu powolna i nieprzyjemna.
Z drugiej strony dobre podejście asynchroniczne prawdopodobnie nie jest szybsze, ale nie jest też najgorsze, moja aplikacja po prostu czeka na wynik i nie zawiesza się, reaguje i trwa znacznie lepsze skalowanie.
Odkryłem również, że zmiana kontekstu w świecie wątków nie jest tak tania w prawdziwym świecie, w rzeczywistości jest dość droga, zwłaszcza gdy masz więcej niż 2 wątki, które muszą się cyklicznie zamieniać i zamieniać między sobą, aby zostać obliczonym.
Na współczesnych procesorach sytuacja nie jest tak różna, kontroler pamięci jest zintegrowany, ale chodzi mi o to, że procesory x86 to w zasadzie maszyna szeregowa, a kontroler pamięci działa tak samo, jak w przypadku starej maszyny z zewnętrznym kontrolerem pamięci na płycie głównej . Zmiana kontekstu jest nadal istotnym kosztem w mojej aplikacji, a fakt, że kontroler pamięci jest zintegrowany lub że nowszy procesor ma więcej niż 2 rdzenie, nie jest dla mnie okazją.
Ponieważ to, czego doświadczyłem, równoczesne podejście jest dobre w teorii, ale nie tak dobre w praktyce, z modelem pamięci narzuconym przez sprzęt, trudno jest dobrze wykorzystać ten paradygmat, a także wprowadza wiele problemów, począwszy od użycia moich struktur danych do połączenia wielu wątków.
Oba paradygmaty również nie oferują żadnego zabezpieczenia, gdy zadanie lub zadanie zostanie wykonane w określonym momencie, co czyni je naprawdę podobnymi z funkcjonalnego punktu widzenia.
Według modelu pamięci X86, dlaczego większość ludzi sugeruje stosowanie współbieżności z C ++, a nie tylko podejście asynchroniczne? A dlaczego nie rozważyć najgorszego scenariusza komputera, w którym zmiana kontekstu jest prawdopodobnie droższa niż samo obliczenie?
źródło
Odpowiedzi:
Masz wiele rdzeni / procesorów, używaj ich
Async jest najlepszy do wykonywania ciężkich operacji związanych z operacjami wejścia / wyjścia, ale co z przetwarzaniem dużych operacji procesora?
Problem powstaje, gdy jednowątkowe bloki kodu (tzn. Utkną) w długotrwałym procesie. Na przykład pamiętasz, kiedy drukowanie dokumentu edytora tekstu powoduje zawieszenie się całej aplikacji, dopóki zadanie nie zostanie wysłane? Zamrożenie aplikacji jest efektem ubocznym blokowania aplikacji jednowątkowej podczas zadania wymagającego dużej mocy procesora.
W aplikacji wielowątkowej zadania intensywnie wykorzystujące procesor (np. Zadanie drukowania) mogą być wysyłane do wątku roboczego w tle, uwalniając w ten sposób wątek interfejsu użytkownika.
Podobnie w aplikacji wieloprocesowej zadanie może zostać wysłane za pośrednictwem wiadomości (np. IPC, gniazda itp.) Do podprocesu zaprojektowanego specjalnie do przetwarzania zadań.
W praktyce każdy kod asynchroniczny i wielowątkowy / procesowy ma swoje zalety i wady.
Widać trend na głównych platformach chmurowych, ponieważ będą one oferować instancje wyspecjalizowane w przetwarzaniu związanym z procesorem i instancje wyspecjalizowane w przetwarzaniu związanym z IO.
Przykłady:
Aby spojrzeć na to z perspektywy ...
Serwer WWW jest doskonałym przykładem platformy silnie związanej z IO. Wielowątkowy serwer WWW, który przypisuje jeden wątek na połączenie, nie skaluje się dobrze, ponieważ każdy wątek wiąże się z większym obciążeniem z powodu zwiększonej ilości przełączania kontekstu i blokowania wątków w zasobach współdzielonych. Natomiast asynchroniczny serwer WWW użyłby jednej przestrzeni adresowej.
Podobnie, aplikacja specjalizująca się w kodowaniu wideo działałaby znacznie lepiej w środowisku wielowątkowym, ponieważ ciężkie przetwarzanie wymagałoby zablokowania głównego wątku do czasu zakończenia pracy. Istnieją sposoby na złagodzenie tego, ale o wiele łatwiej jest mieć jeden wątek zarządzający kolejką, drugi wątek zarządzający czyszczeniem i pulę wątków zarządzającą intensywnym przetwarzaniem. Komunikacja między wątkami ma miejsce tylko wtedy, gdy zadania są przypisane / zakończone, więc narzut blokowania wątków jest ograniczony do absolutnego minimum.
Najlepsza aplikacja często wykorzystuje kombinację obu. Na przykład aplikacja internetowa może używać nginx (tj. Asynchroniczny jednowątkowy) jako modułu równoważenia obciążenia do zarządzania torrentem przychodzących żądań, podobny asynchroniczny serwer WWW (np. Node.js) do obsługi żądań HTTP oraz zestaw serwerów wielowątkowych obsłużyć przesyłanie / przesyłanie strumieniowe / kodowanie treści itp.
Przez lata toczyło się wiele wojen religijnych między modelami wielowątkowymi, wieloprocesowymi i asynchronicznymi. Tak jak w przypadku większości rzeczy, najlepszą odpowiedzią powinno być „to zależy”.
Wynika to z tego samego sposobu myślenia, który uzasadnia jednoczesne użycie architektury GPU i CPU. Dwa wyspecjalizowane systemy działające wspólnie mogą mieć znacznie większą poprawę niż pojedyncze podejście monolityczne.
Żadne z nich nie jest lepsze, ponieważ oba mają swoje zastosowania. Użyj najlepszego narzędzia do pracy.
Aktualizacja:
Usunąłem odniesienie do Apache i wprowadziłem niewielką korektę. Apache używa modelu wieloprocesowego, który forsuje proces dla każdego żądania, zwiększając ilość przełączania kontekstu na poziomie jądra. Ponadto, ponieważ pamięci nie można współdzielić między procesami, każde żądanie wiąże się z dodatkowym kosztem pamięci.
Wątek wielowątkowy wymaga dodatkowej pamięci, ponieważ opiera się na pamięci współdzielonej między wątkami. Pamięć współdzielona usuwa dodatkowy narzut pamięci, ale wciąż podlega karze zwiększonego przełączania kontekstu. Ponadto - aby upewnić się, że warunki wyścigu się nie zdarzają - blokady wątków (które zapewniają wyłączny dostęp tylko do jednego wątku na raz) są wymagane dla wszystkich zasobów, które są współużytkowane przez wątki.
To zabawne, że mówisz: „programiści wydają się lubić współbieżność i programy wielowątkowe w ogóle”. Programowanie wielowątkowe jest powszechnie przerażane przez każdego, kto wykonał znaczną jego część w swoim czasie. Martwe zamki (błąd, który występuje, gdy zasób jest omyłkowo zablokowany przez dwa różne źródła blokujące oba od zawsze kończące się) i warunki wyścigu (w których program błędnie losowo wyprowadza nieprawidłowy wynik z powodu nieprawidłowego sekwencjonowania) są jednymi z najtrudniejszych do śledzenia w dół i naprawić.
Aktualizacja 2:
W przeciwieństwie do ogólnego stwierdzenia, że IPC jest szybsze niż komunikacja sieciowa (tj. Gniazdowa). Nie zawsze tak jest . Należy pamiętać, że są to uogólnienia, a szczegóły dotyczące implementacji mogą mieć ogromny wpływ na wynik.
źródło
Asynchroniczne podejście firmy Microsoft stanowi dobry zamiennik najczęstszych celów programowania wielowątkowego: poprawy zdolności reagowania w odniesieniu do zadań we / wy.
Należy jednak pamiętać, że podejście asynchroniczne nie jest w stanie w ogóle poprawić wydajności ani poprawić odpowiedzi w odniesieniu do zadań intensywnie wykorzystujących procesor.
Wielowątkowość dla responsywności
Wielowątkowość dla responsywności to tradycyjny sposób na utrzymanie reakcji programu podczas ciężkich zadań we / wy lub ciężkich zadań obliczeniowych. Zapisujesz pliki w wątku w tle, aby użytkownik mógł kontynuować pracę bez konieczności oczekiwania na zakończenie pracy dysku twardego. Wątek We / Wy często blokuje oczekiwanie na zakończenie zapisu, więc częste są zmiany kontekstu.
Podobnie, wykonując złożone obliczenia, chcesz umożliwić regularne przełączanie kontekstu, aby interfejs użytkownika pozostawał responsywny, a użytkownik nie sądzi, że program się zawiesił.
Celem nie jest generalnie uruchomienie wielu wątków na różnych procesorach. Zamiast tego interesuje nas po prostu przełączanie kontekstu między długo działającym zadaniem w tle a interfejsem użytkownika, aby interfejs użytkownika mógł aktualizować i odpowiadać użytkownikowi podczas działania zadania w tle. Ogólnie interfejs użytkownika nie pobiera dużej mocy procesora, a struktura wątków lub system operacyjny zazwyczaj decydują się na uruchomienie ich na tym samym procesorze.
W rzeczywistości tracimy ogólną wydajność z powodu dodatkowych kosztów przełączania kontekstu, ale nie obchodzi nas to, ponieważ wydajność procesora nie była naszym celem. Wiemy, że zwykle mamy więcej mocy procesora niż potrzebujemy, dlatego naszym celem w zakresie wielowątkowości jest wykonanie zadania dla użytkownika bez marnowania czasu użytkownika.
„Asynchroniczna” alternatywa
„Podejście asynchroniczne” zmienia ten obraz, umożliwiając przełączanie kontekstu w ramach jednego wątku. Gwarantuje to, że wszystkie nasze zadania będą działały na jednym procesorze, i może zapewnić niewielką poprawę wydajności pod względem mniejszej liczby tworzenia / czyszczenia wątków i mniejszej liczby rzeczywistych przełączeń kontekstu między wątkami.
Zamiast tworzyć nowy wątek, który będzie oczekiwał na odbiór zasobu sieciowego (np. Pobranie obrazu),
async
stosowana jest metoda, dzięki którejawait
obraz staje się dostępny, a tymczasem ulega metodzie wywoływania.Główną zaletą jest to, że nie musisz martwić się o problemy z wątkami, takie jak unikanie zakleszczenia, ponieważ w ogóle nie używasz blokad i synchronizacji, a programista konfiguruje wątek w tle i wraca z powrotem w wątku interfejsu użytkownika, gdy wynik wraca w celu bezpiecznej aktualizacji interfejsu użytkownika.
Nie zagłębiłem się zbytnio w szczegóły techniczne, ale mam wrażenie, że zarządzanie pobieraniem przy sporadycznej lekkiej aktywności procesora staje się zadaniem nie dla osobnego wątku, ale raczej czymś w rodzaju zadania w kolejce zdarzeń interfejsu użytkownika i kiedy pobieranie zostało zakończone, metoda asynchroniczna jest wznawiana z kolejki zdarzeń. Innymi słowy,
await
oznacza coś w rodzaju „sprawdź, czy potrzebny wynik jest dostępny, jeśli nie, umieść mnie z powrotem w kolejce zadań tego wątku”.Zauważ, że takie podejście nie rozwiązałoby problemu zadania intensywnie wykorzystującego procesor: nie ma danych, na które trzeba czekać, więc nie możemy uzyskać przełączników kontekstu, których potrzebujemy, bez utworzenia rzeczywistego wątku roboczego w tle. Oczywiście nadal wygodne może być użycie metody asynchronicznej do uruchomienia wątku w tle i zwrócenia wyniku w programie, który szeroko wykorzystuje podejście asynchroniczne.
Wielowątkowość dla wydajności
Ponieważ mówisz o „wydajności”, chciałbym również omówić, w jaki sposób wielowątkowość można wykorzystać do zwiększenia wydajności, co jest całkowicie niemożliwe w przypadku jednowątkowego podejścia asynchronicznego.
Kiedy faktycznie znajdujesz się w sytuacji, gdy nie masz wystarczającej mocy procesora na jednym procesorze i chcesz użyć wielowątkowości do wydajności, jest to często trudne. Z drugiej strony, jeśli jeden procesor nie ma wystarczającej mocy obliczeniowej, jest to często jedyne rozwiązanie, które może umożliwić Twojemu programowi wykonanie tego, co chciałbyś osiągnąć w rozsądnym czasie, co sprawia, że praca jest tego warta.
Trywialna równoległość
Oczywiście czasami uzyskanie szybkiego przyspieszenia z wielowątkowości może być łatwe.
Jeśli zdarzy się, że masz dużą liczbę niezależnych zadań wymagających intensywnych obliczeń (to znaczy zadań, których dane wejściowe i wyjściowe są bardzo małe w stosunku do obliczeń, które należy wykonać w celu ustalenia wyniku), wówczas często można uzyskać znaczne przyspieszenie poprzez tworzenie puli wątków (odpowiednio dobranych na podstawie liczby dostępnych procesorów) i posiadanie wątku głównego rozprowadza pracę i zbiera wyniki.
Praktyczny wielowątkowość dla wydajności
Nie chcę się przedstawiać jako ekspert, ale mam wrażenie, że ogólnie rzecz biorąc, najbardziej praktycznym wielowątkowością dla wydajności, która ma miejsce obecnie, jest szukanie miejsc w aplikacji o trywialnej równoległości i używanie wielu wątków czerpać korzyści.
Jak w przypadku każdej optymalizacji, zwykle lepiej jest zoptymalizować po wyprofilowaniu wydajności programu i zidentyfikowaniu gorących punktów: łatwo jest spowolnić program, decydując arbitralnie, że ta część powinna działać w jednym wątku, a ta w innym, bez najpierw określając, czy obie części zajmują znaczną część czasu procesora.
Dodatkowy wątek oznacza większe koszty konfiguracji / porzucenia oraz więcej przełączników kontekstu lub więcej kosztów komunikacji między procesorami. Jeśli nie robi wystarczająco dużo pracy, aby zrekompensować te koszty, jeśli jest na osobnym procesorze i nie musi być osobnym wątkiem ze względu na szybkość reakcji, spowolni to bez żadnych korzyści.
Poszukaj zadań, które mają niewiele współzależności i które zajmują znaczną część czasu wykonywania programu.
Jeśli nie mają wzajemnych zależności, to jest to trywialny paralelizm, możesz łatwo ustawić każdy z wątkiem i cieszyć się korzyściami.
Jeśli możesz znaleźć zadania o ograniczonej współzależności, aby blokowanie i synchronizacja w celu wymiany informacji nie spowalniały ich znacznie, wówczas wielowątkowość może nieco przyspieszyć, pod warunkiem, że unikasz niebezpieczeństw związanych z błędem logicznym podczas synchronizacji lub niepoprawne wyniki z powodu braku synchronizacji, gdy jest to konieczne.
Alternatywnie, niektóre z bardziej powszechnych aplikacji do wielowątkowości nie szukają (w pewnym sensie) przyspieszenia z góry określonego algorytmu, ale zamiast tego mają większy budżet na algorytm, który planują napisać: jeśli piszesz silnik gry , a twoja sztuczna inteligencja musi podjąć decyzję w ramach liczby klatek na sekundę, często możesz zwiększyć swoją inteligencję budżetu cyklu CPU, jeśli możesz dać jej własny procesor.
Pamiętaj jednak, aby wyprofilować wątki i upewnić się, że wykonują wystarczająco dużo pracy, aby w pewnym momencie zrekompensować koszty.
Algorytmy równoległe
Istnieje również wiele problemów, które można przyspieszyć za pomocą wielu procesorów, ale są one zbyt monolityczne, aby po prostu podzielić je na procesory.
Algorytmy równoległe muszą być dokładnie analizowane pod kątem czasu działania dużych O w odniesieniu do najlepszego dostępnego algorytmu nierównoległego, ponieważ koszt komunikacji między procesorami jest bardzo łatwy w celu wyeliminowania jakichkolwiek korzyści z używania wielu procesorów. Zasadniczo muszą używać mniejszej komunikacji między procesorami (w kategoriach dużych O) niż obliczeń na każdym procesorze.
W tej chwili nadal jest to w dużej mierze przestrzeń do badań akademickich, częściowo ze względu na wymaganą złożoną analizę, częściowo ze względu na dość powszechną banalną równoległość, częściowo dlatego, że nie mamy jeszcze tak wielu rdzeni procesora na naszych komputerach, że problemy, które nie można rozwiązać w rozsądnym czasie na jednym procesorze można rozwiązać w rozsądnym czasie przy użyciu wszystkich naszych procesorów.
źródło
I jest twój problem. Responsywny interfejs użytkownika nie tworzy wydajnej aplikacji. Często odwrotnie. Sporo czasu spędza się na sprawdzaniu danych wejściowych w interfejsie użytkownika zamiast na wykonywaniu zadań przez wątki robocze.
Jeśli chodzi o „tylko” podejście asynchroniczne, to również wielowątkowość, choć poprawiona dla tego konkretnego przypadku użycia w większości środowisk . W innych, asynchronizacja odbywa się za pomocą firm, które ... nie zawsze są współbieżne.
Szczerze mówiąc, uważam, że operacje asynchroniczne są trudniejsze do uzasadnienia i użycia w sposób, który faktycznie zapewnia korzyści (wydajność, niezawodność, łatwość konserwacji), nawet w porównaniu z ... bardziej ręcznym podejściem.
źródło