Uczę się, jak używać threading
i te multiprocessing
moduły w Pythonie, aby uruchomić pewne operacje równolegle i przyspieszyć mój kod.
Trudno mi (być może dlatego, że nie mam żadnych podstaw teoretycznych), aby zrozumieć, jaka jest różnica między threading.Thread()
obiektem a multiprocessing.Process()
jedynką.
Ponadto nie jest dla mnie całkowicie jasne, jak utworzyć wystąpienie kolejki zadań i mieć tylko 4 (na przykład) z nich uruchomionych równolegle, podczas gdy inne czekają na zwolnienie zasobów przed wykonaniem.
Uważam, że przykłady w dokumentacji są jasne, ale niezbyt wyczerpujące; gdy tylko próbuję trochę komplikować, otrzymuję wiele dziwnych błędów (takich jak metoda, której nie można wytrawić itd.).
Kiedy więc powinienem używać modułów threading
i multiprocessing
?
Czy możesz połączyć mnie z niektórymi zasobami, które wyjaśniają koncepcje tych dwóch modułów i jak ich właściwie używać do złożonych zadań?
Thread
moduł (nazwany_thread
w Pythonie 3.x). Szczerze mówiąc, nigdy nie rozumiał różnice ja ...Thread
/_thread
dokumentacja, są to „prymitywy niskiego poziomu”. Możesz go użyć do zbudowania niestandardowych obiektów synchronizacji, do kontrolowania kolejności łączenia drzewa wątków itp. Jeśli nie możesz sobie wyobrazić, dlaczego miałbyś go używać, nie używaj go i trzymaj sięthreading
.Odpowiedzi:
To, co mówi Giulio Franco, odnosi się do wielowątkowości w porównaniu z wielowątkowością w ogóle .
Jednak Python * ma dodatkowy problem: istnieje globalna blokada interpretera, która uniemożliwia dwóm wątkom w tym samym procesie uruchamianie kodu Pythona w tym samym czasie. Oznacza to, że jeśli masz 8 rdzeni i zmienisz kod tak, aby korzystał z 8 wątków, nie będzie on w stanie wykorzystać 800% procesora i działać 8 razy szybciej; będzie używać tego samego 100% procesora i działać z tą samą prędkością. (W rzeczywistości będzie działać trochę wolniej, ponieważ wątkowanie wiąże się z dodatkowymi kosztami, nawet jeśli nie masz żadnych udostępnionych danych, ale na razie zignoruj to).
Są od tego wyjątki. Jeśli intensywne obliczenia twojego kodu w rzeczywistości nie są wykonywane w Pythonie, ale w jakiejś bibliotece z niestandardowym kodem C, która wykonuje właściwą obsługę GIL, jak aplikacja numpy, otrzymasz oczekiwaną korzyść wydajności z wątków. To samo dotyczy sytuacji, gdy ciężkie obliczenia są wykonywane przez jakiś podproces, który uruchamiasz i na który czekasz.
Co ważniejsze, są przypadki, w których to nie ma znaczenia. Na przykład serwer sieciowy spędza większość czasu na odczytywaniu pakietów z sieci, a aplikacja GUI spędza większość czasu na czekaniu na zdarzenia użytkownika. Jednym z powodów używania wątków w serwerze sieciowym lub aplikacji z graficznym interfejsem użytkownika jest umożliwienie wykonywania długotrwałych „zadań w tle” bez zatrzymywania kontynuowania obsługi pakietów sieciowych lub zdarzeń GUI przez główny wątek. I to działa dobrze z wątkami Pythona. (Z technicznego punktu widzenia oznacza to, że wątki Pythona zapewniają współbieżność, mimo że nie zapewniają równoległości rdzenia).
Ale jeśli piszesz program związany z procesorem w czystym Pythonie, używanie większej liczby wątków na ogół nie jest pomocne.
Stosowanie oddzielnych procesów nie stwarza takich problemów w GIL, ponieważ każdy proces ma swój własny, oddzielny GIL. Oczywiście nadal masz takie same kompromisy między wątkami i procesami, jak w każdym innym języku - udostępnianie danych między procesami jest trudniejsze i droższe niż między wątkami, uruchamianie ogromnej liczby procesów lub tworzenie i niszczenie może być kosztowne je często itd. Ale GIL w dużym stopniu waży na szali w procesach, w sposób, który nie jest prawdziwy, powiedzmy, C czy Java. Dlatego w Pythonie znacznie częściej będziesz korzystać z przetwarzania wieloprocesowego niż w C czy Javie.
W międzyczasie filozofia Pythona „w zestawie baterie” przynosi dobre wieści: bardzo łatwo jest napisać kod, który można przełączać między wątkami i procesami za pomocą jednej linii.
Jeśli projektujesz swój kod w kategoriach samodzielnych "zadań", które nie współużytkują niczego z innymi zadaniami (lub programem głównym) poza danymi wejściowymi i wyjściowymi, możesz użyć
concurrent.futures
biblioteki do napisania swojego kodu wokół puli wątków w następujący sposób:Możesz nawet pobrać wyniki tych zadań i przekazać je do dalszych zadań, czekać na rzeczy w kolejności wykonania lub kolejności ukończenia itp .; przeczytaj sekcję o
Future
obiektach po szczegóły.Teraz, jeśli okaże się, że twój program stale używa 100% procesora, a dodanie większej liczby wątków tylko go spowolni, oznacza to, że masz problem z GIL, więc musisz przełączyć się na procesy. Wszystko, co musisz zrobić, to zmienić pierwszą linię:
Jedynym prawdziwym zastrzeżeniem jest to, że argumenty i wartości zwracane zadań muszą być trawione (i nie zajmować zbyt dużo czasu ani pamięci), aby można je było wykorzystać w procesach krzyżowych. Zwykle nie stanowi to problemu, ale czasami jest.
Ale co, jeśli twoja praca nie może być samowystarczalna? Jeśli potrafisz zaprojektować swój kod pod kątem zadań, które przekazują wiadomości od jednego do drugiego, nadal jest to całkiem proste. Być może będziesz musiał użyć
threading.Thread
lubmultiprocessing.Process
zamiast polegać na pulach. I trzeba będzie utworzyćqueue.Queue
lubmultiprocessing.Queue
obiekty wyraźnie. (Jest wiele innych opcji - potoki, gniazda, pliki ze stokami ... ale chodzi o to, że musisz zrobić coś ręcznie, jeśli automatyczna magia Executora jest niewystarczająca.)Ale co, jeśli nie możesz nawet polegać na przekazywaniu wiadomości? A jeśli potrzebujesz dwóch miejsc pracy, aby zmutować tę samą strukturę i zobaczyć zmiany innych? W takim przypadku będziesz musiał wykonać ręczną synchronizację (blokady, semafory, warunki itp.) I, jeśli chcesz używać procesów, jawne obiekty pamięci współdzielonej do rozruchu. Dzieje się tak, gdy wielowątkowość (lub wieloprocesowość) staje się trudna. Jeśli możesz tego uniknąć, świetnie; jeśli nie możesz, będziesz musiał przeczytać więcej, niż ktoś może udzielić odpowiedzi TAK.
Z komentarza chciałeś wiedzieć, czym różnią się wątki i procesy w Pythonie. Naprawdę, jeśli przeczytasz odpowiedź Giulio Franco, moją i wszystkie nasze linki, to powinno obejmować wszystko… ale podsumowanie z pewnością byłoby przydatne, więc oto:
ctypes
typy.threading
Moduł nie posiada niektórych cechmultiprocessing
modułu. (Możesz użyć,multiprocessing.dummy
aby uzyskać większość brakującego interfejsu API na wierzchu wątków, lub możesz użyć modułów wyższego poziomu, takich jakconcurrent.futures
i nie martw się o to.)* Właściwie to nie Python, język, ma ten problem, ale CPython, „standardowa” implementacja tego języka. Niektóre inne implementacje nie mają GIL, jak Jython.
** Jeśli używasz metody fork start do przetwarzania wieloprocesowego - co jest możliwe na większości platform innych niż Windows - każdy proces potomny otrzymuje zasoby, które posiadał rodzic, gdy dziecko zostało uruchomione, co może być innym sposobem przekazywania danych dzieciom.
źródło
pickle
dokumentacja wytłumaczyć), a następnie w najgorszym twój głupi wrapper jestdef wrapper(obj, *args): return obj.wrapper(*args)
.W jednym procesie może istnieć wiele wątków. Wątki należące do tego samego procesu współużytkują ten sam obszar pamięci (mogą czytać i zapisywać do tych samych zmiennych oraz mogą wzajemnie się zakłócać). Wręcz przeciwnie, w różnych obszarach pamięci istnieją różne procesy, a każdy z nich ma swoje własne zmienne. Aby się komunikować, procesy muszą używać innych kanałów (plików, potoków lub gniazd).
Jeśli chcesz zrównoleglać obliczenia, prawdopodobnie będziesz potrzebować wielowątkowości, ponieważ prawdopodobnie chcesz, aby wątki współpracowały w tej samej pamięci.
Mówiąc o wydajności, wątki są szybsze w tworzeniu i zarządzaniu niż procesy (ponieważ system operacyjny nie musi przydzielać zupełnie nowego obszaru pamięci wirtualnej), a komunikacja między wątkami jest zwykle szybsza niż komunikacja między procesami. Ale wątki są trudniejsze do zaprogramowania. Wątki mogą ze sobą kolidować i mogą zapisywać się nawzajem w pamięci, ale sposób, w jaki to się dzieje, nie zawsze jest oczywisty (ze względu na kilka czynników, głównie zmianę kolejności instrukcji i buforowanie pamięci), dlatego do kontroli dostępu będą potrzebne prymitywy synchronizacji do swoich zmiennych.
źródło
threading
imultiprocessing
.Uważam, że ten link w elegancki sposób odpowiada na Twoje pytanie.
Krótko mówiąc, jeśli jeden z podproblemów musi czekać, aż inny się zakończy, wielowątkowość jest dobra (na przykład w ciężkich operacjach we / wy); Z drugiej strony, jeśli twoje podproblemy naprawdę mogą się zdarzyć w tym samym czasie, sugerowane jest przetwarzanie wieloprocesowe. Jednak nie utworzysz więcej procesów niż liczba rdzeni.
źródło
Cytaty z dokumentacji Pythona
Podkreśliłem kluczowe cytaty z dokumentacji Pythona dotyczące Process vs Threads i GIL na: Co to jest globalna blokada interpretera (GIL) w CPythonie?
Eksperymenty z procesami a wątkami
Przeprowadziłem trochę benchmarkingu, aby bardziej konkretnie pokazać różnicę.
W teście porównawczym ustaliłem czas pracy związanej z procesorem i we / wy dla różnych liczb wątków na 8-wątkowym procesorze. Praca dostarczana na wątek jest zawsze taka sama, więc więcej wątków oznacza większą całkowitą dostarczoną pracę.
Wyniki były następujące:
Dane wykresu .
Wnioski:
w przypadku pracy związanej z procesorem wieloprocesorowość jest zawsze szybsza, prawdopodobnie dzięki GIL
do pracy związanej z IO. obie mają dokładnie taką samą prędkość
wątki skalują się tylko do około 4x zamiast oczekiwanego 8x, ponieważ jestem na maszynie 8-wątkowej.
Porównaj to z pracą związaną z procesorem C-POSIX, która osiąga oczekiwane 8-krotne przyspieszenie: co oznaczają „rzeczywisty”, „użytkownik” i „sys” w danych wyjściowych czasu (1)?
DO ZROBIENIA: Nie wiem, dlaczego tak się dzieje, muszą pojawić się inne problemy z wydajnością Pythona.
Kod testowy:
GitHub upstream + kreślenie kodu w tym samym katalogu .
Testowano na Ubuntu 18.10, Python 3.6.7, w laptopie Lenovo ThinkPad P51 z procesorem: procesor Intel Core i7-7820HQ (4 rdzenie / 8 wątków), pamięć RAM: 2x Samsung M471A2K43BB1-CRC (2x 16GiB), dysk SSD: Samsung MZVLB512HAJQ- 000L7 (3000 MB / s).
Wizualizuj, które wątki działają w danym momencie
Ten post https://rohanvarma.me/GIL/ nauczył mnie, że możesz uruchomić wywołanie zwrotne za każdym razem, gdy zaplanowany jest wątek z
target=
argumentemthreading.Thread
i to samo formultiprocessing.Process
.To pozwala nam zobaczyć dokładnie, który wątek jest uruchamiany za każdym razem. Po wykonaniu tej czynności zobaczylibyśmy coś takiego (zrobiłem ten konkretny wykres):
co by pokazało, że:
źródło
Oto kilka danych dotyczących wydajności dla języka Python 2.6.x, które podważają pogląd, że wątkowanie jest bardziej wydajne niż przetwarzanie wieloprocesowe w scenariuszach związanych z IO. Te wyniki pochodzą z 40-procesorowego IBM System x3650 M4 BD.
Przetwarzanie powiązane we / wy: pula procesów działa lepiej niż pula wątków
Przetwarzanie związane z procesorem: Pula procesów działa lepiej niż pula wątków
Nie są to rygorystyczne testy, ale mówią mi, że przetwarzanie wieloprocesowe nie jest całkowicie nieefektywne w porównaniu z wątkami.
Kod używany w interaktywnej konsoli Pythona do powyższych testów
źródło
>>> do_work(50, 300, 'thread', 'fileio') --> 237.557 ms
>>> do_work(50, 300, 'process', 'fileio') --> 323.963 ms
>>> do_work(50, 2000, 'thread', 'square') --> 232.082 ms
>>> do_work(50, 2000, 'process', 'square') --> 282.785 ms
Cóż, na większość pytań odpowiada Giulio Franco. W dalszej części opiszę problem konsument-producent, który, jak sądzę, poprowadzi Cię na właściwą ścieżkę do rozwiązania problemu korzystania z aplikacji wielowątkowej.
Możesz przeczytać więcej na temat prymitywów synchronizacji z:
Pseudokod jest powyżej. Przypuszczam, że powinieneś przeszukać problem producent-konsument, aby uzyskać więcej referencji.
źródło