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:
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
executor.submit(job, argument)
executor.map(some_function, collection_of_independent_things)
# ...
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ę:
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
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
lub multiprocessing.Process
zamiast polegać na pulach. I trzeba będzie utworzyć queue.Queue
lub multiprocessing.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:
- Wątki domyślnie udostępniają dane; procesy nie.
- W konsekwencji (1) przesyłanie danych między procesami wymaga zazwyczaj wytrawiania i wytrawiania. **
- Inną konsekwencją (1) jest bezpośrednie udostępnianie danych między procesami na ogół wymaga umieszczenia ich w formatach niskiego poziomu, takich jak wartość, tablica i
ctypes
typy.
- Procesy nie podlegają GIL.
- Na niektórych platformach (głównie Windows) tworzenie i niszczenie procesów jest znacznie droższe.
- Istnieją dodatkowe ograniczenia dotyczące procesów, z których niektóre są różne na różnych platformach. Szczegółowe informacje można znaleźć w wytycznych dotyczących programowania .
threading
Moduł nie posiada niektórych cech multiprocessing
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 jak concurrent.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.
Thread
moduł (nazwany_thread
w Pythonie 3.x). Szczerze mówiąc, nigdy nie rozumiał różnice ja ...