Współbieżność w Pythonie - szybki przewodnik
W tym rozdziale zrozumiemy pojęcie współbieżności w Pythonie i poznamy różne wątki i procesy.
Co to jest współbieżność?
Krótko mówiąc, współbieżność to występowanie dwóch lub więcej zdarzeń w tym samym czasie. Współbieżność jest zjawiskiem naturalnym, ponieważ w danym momencie występuje jednocześnie wiele zdarzeń.
Jeśli chodzi o programowanie, współbieżność występuje, gdy dwa zadania nakładają się na siebie podczas wykonywania. Dzięki programowaniu współbieżnemu można poprawić wydajność naszych aplikacji i systemów oprogramowania, ponieważ możemy jednocześnie obsługiwać żądania, zamiast czekać na ukończenie poprzedniego.
Historyczny przegląd współbieżności
Poniższe punkty dadzą nam krótki historyczny przegląd współbieżności -
Z koncepcji kolei
Współbieżność jest ściśle związana z koncepcją kolei. W przypadku kolei istniała potrzeba obsługi wielu pociągów w tym samym systemie kolejowym w taki sposób, aby każdy pociąg bezpiecznie dotarł do celu.
Obliczenia współbieżne w środowisku akademickim
Zainteresowanie współbieżnością w informatyce zaczęło się od artykułu badawczego opublikowanego przez Edsgera W. Dijkstry w 1965 roku. W artykule tym zidentyfikował i rozwiązał problem wzajemnego wykluczania, będącego własnością kontroli współbieżności.
Prymitywy współbieżności wysokiego poziomu
W ostatnim czasie programiści otrzymują ulepszone rozwiązania współbieżne z powodu wprowadzenia prymitywów współbieżności wysokiego poziomu.
Ulepszona współbieżność z językami programowania
Języki programowania, takie jak Golang, Rust i Python firmy Google, dokonały niesamowitych zmian w obszarach, które pomagają nam uzyskać lepsze, współbieżne rozwiązania.
Co to jest wątek i wielowątkowość?
Threadto najmniejsza jednostka wykonania, jaką można wykonać w systemie operacyjnym. Sam w sobie nie jest programem, ale działa w programie. Innymi słowy, wątki nie są od siebie niezależne. Każdy wątek dzieli sekcję kodu, sekcję danych itp. Z innymi wątkami. Znane są również jako lekkie procesy.
Wątek składa się z następujących elementów -
Licznik programu, który składa się z adresu następnej instrukcji wykonywalnej
Stack
Zestaw rejestrów
Unikalny identyfikator
MultithreadingZ drugiej strony to zdolność procesora do zarządzania wykorzystaniem systemu operacyjnego poprzez jednoczesne wykonywanie wielu wątków. Główną ideą wielowątkowości jest osiągnięcie równoległości poprzez podzielenie procesu na wiele wątków. Pojęcie wielowątkowości można zrozumieć na poniższym przykładzie.
Przykład
Załóżmy, że uruchamiamy określony proces, w którym otwieramy MS Word, aby wpisać do niego zawartość. Jeden wątek zostanie przypisany do otwarcia MS Word, a inny wątek będzie wymagany do wpisania w nim treści. A teraz, jeśli chcemy edytować istniejący, do wykonania zadania edycji będzie potrzebny kolejny wątek i tak dalej.
Co to jest proces i wieloprocesorowość?
ZAprocessdefiniuje się jako jednostkę, która stanowi podstawową jednostkę pracy do zaimplementowania w systemie. Mówiąc prościej, piszemy nasze programy komputerowe w pliku tekstowym i kiedy wykonujemy ten program, staje się on procesem, który wykonuje wszystkie zadania wymienione w programie. Podczas cyklu życia procesu przechodzi przez różne etapy - Start, Gotowość, Uruchomienie, Oczekiwanie i Zakończenie.
Poniższy diagram przedstawia różne etapy procesu -
Proces może mieć tylko jeden wątek, zwany wątkiem podstawowym, lub wiele wątków posiadających własny zestaw rejestrów, licznik programu i stos. Poniższy diagram pokaże nam różnicę -
Multiprocessing,z drugiej strony jest to użycie dwóch lub więcej jednostek CPU w jednym systemie komputerowym. Naszym głównym celem jest wykorzystanie pełnego potencjału naszego sprzętu. Aby to osiągnąć, musimy wykorzystać pełną liczbę rdzeni procesora dostępnych w naszym systemie komputerowym. Najlepszym podejściem do tego jest wieloprocesowość.
Python to jeden z najpopularniejszych języków programowania. Oto kilka powodów, które sprawiają, że nadaje się do jednoczesnych aplikacji -
Cukier syntaktyczny
Cukier syntaktyczny to składnia w języku programowania, która ma na celu ułatwienie czytania lub wyrażania. Sprawia, że język jest „słodszy” do użytku przez ludzi: rzeczy można wyrazić jaśniej, bardziej zwięźle lub w alternatywnym stylu opartym na preferencjach. Python zawiera metody Magic, które można zdefiniować tak, aby działały na obiektach. Te magiczne metody są używane jako cukier syntaktyczny i są powiązane z łatwiejszymi do zrozumienia słowami kluczowymi.
Duża społeczność
Język Python był świadkiem ogromnego wskaźnika adopcji wśród analityków danych i matematyków, pracujących w dziedzinie sztucznej inteligencji, uczenia maszynowego, głębokiego uczenia się i analizy ilościowej.
Przydatne interfejsy API do programowania współbieżnego
Python 2 i 3 mają dużą liczbę interfejsów API przeznaczonych do programowania równoległego / współbieżnego. Najpopularniejsze z nich tothreading, concurrent.features, multiprocessing, asyncio, gevent and greenlets, itp.
Ograniczenia Pythona we wdrażaniu współbieżnych aplikacji
Python zawiera ograniczenie dla współbieżnych aplikacji. To ograniczenie nazywa sięGIL (Global Interpreter Lock)jest obecny w Pythonie. GIL nigdy nie pozwala nam na wykorzystanie wielu rdzeni procesora, dlatego możemy powiedzieć, że w Pythonie nie ma prawdziwych wątków. Możemy zrozumieć koncepcję GIL w następujący sposób -
GIL (Global Interpreter Lock)
Jest to jeden z najbardziej kontrowersyjnych tematów w świecie Pythona. W CPythonie GIL jest muteksem - blokadą wzajemnego wykluczania, która zapewnia bezpieczeństwo wątków. Innymi słowy, możemy powiedzieć, że GIL zapobiega równoległemu wykonywaniu kodu Pythona przez wiele wątków. Blokada może być utrzymywana tylko przez jeden wątek naraz, a jeśli chcemy wykonać wątek, musi najpierw uzyskać blokadę. Poniższy diagram pomoże ci zrozumieć działanie GIL.
Jednak istnieją pewne biblioteki i implementacje w Pythonie, takie jak Numpy, Jpython i IronPytbhon. Biblioteki te działają bez interakcji z GIL.
Zarówno współbieżność, jak i równoległość są używane w odniesieniu do programów wielowątkowych, ale istnieje wiele nieporozumień dotyczących podobieństwa i różnicy między nimi. Najważniejsze pytanie w tym zakresie: czy równoległość współbieżności jest czy nie? Chociaż oba terminy wydają się dość podobne, ale odpowiedź na powyższe pytanie brzmi NIE, współbieżność i równoległość nie są tym samym. A jeśli nie są takie same, jaka jest między nimi podstawowa różnica?
Mówiąc prościej, współbieżność zajmuje się zarządzaniem dostępem do współdzielonego stanu z różnych wątków, a z drugiej strony równoległość polega na wykorzystaniu wielu procesorów lub ich rdzeni w celu poprawy wydajności sprzętu.
Szczegóły współbieżności
Współbieżność występuje, gdy dwa zadania nakładają się na siebie podczas wykonywania. Może to być sytuacja, w której aplikacja obsługuje więcej niż jedno zadanie w tym samym czasie. Możemy to zrozumieć schematycznie; w tym samym czasie postępuje wiele zadań, jak następuje -
Poziomy współbieżności
W tej sekcji omówimy trzy ważne poziomy współbieżności w zakresie programowania -
Współbieżność niskiego poziomu
Na tym poziomie współbieżności istnieje jawne użycie operacji atomowych. Nie możemy używać tego rodzaju współbieżności do budowania aplikacji, ponieważ jest ona bardzo podatna na błędy i trudna do debugowania. Nawet Python nie obsługuje tego rodzaju współbieżności.
Współbieżność średniego poziomu
W tej współbieżności nie ma użycia jawnych operacji atomowych. Używa jawnych blokad. Python i inne języki programowania obsługują tego rodzaju współbieżność. Przeważnie programiści aplikacji używają tej współbieżności.
Współbieżność wysokiego poziomu
W tej współbieżności nie są używane ani jawne operacje niepodzielne, ani jawne blokady. Python maconcurrent.futures moduł do obsługi tego rodzaju współbieżności.
Właściwości systemów współbieżnych
Aby program lub system współbieżny były poprawne, muszą spełniać pewne właściwości. Właściwości związane z zakończeniem systemu są następujące -
Właściwość poprawności
Właściwość poprawności oznacza, że program lub system musi podać żądaną poprawną odpowiedź. Aby to uprościć, możemy powiedzieć, że system musi poprawnie odwzorować stan programu początkowego na stan końcowy.
Majątek bezpieczeństwa
Właściwość bezpieczeństwa oznacza, że program lub system musi pozostać w pliku “good” lub “safe” stan i nigdy nic nie robi “bad”.
Majątek życia
Ta właściwość oznacza, że program lub system musi “make progress” i osiągnie jakiś pożądany stan.
Aktorzy systemów współbieżnych
Jest to wspólna właściwość systemu współbieżnego, w którym może istnieć wiele procesów i wątków, które działają w tym samym czasie, aby wykonywać postępy w wykonywaniu własnych zadań. Te procesy i wątki nazywane są aktorami systemu współbieżnego.
Zasoby systemów współbieżnych
Aktorzy muszą wykorzystywać zasoby, takie jak pamięć, dysk, drukarka itp., Aby wykonywać swoje zadania.
Pewien zbiór zasad
Każdy współbieżny system musi posiadać zestaw reguł określających rodzaj zadań do wykonania przez aktorów i ich harmonogram. Zadania mogą obejmować pozyskiwanie blokad, udostępnianie pamięci, modyfikowanie stanu itp.
Bariery współbieżnych systemów
Udostępnianie danych
Istotną kwestią podczas wdrażania systemów współbieżnych jest współdzielenie danych pomiędzy wieloma wątkami lub procesami. W rzeczywistości programista musi zapewnić, że blokady chronią współdzielone dane, tak aby wszystkie dostępy do nich były serializowane i tylko jeden wątek lub proces miał dostęp do współdzielonych danych w danym momencie. W przypadku, gdy wiele wątków lub procesów próbuje uzyskać dostęp do tych samych udostępnionych danych, nie wszystkie, z wyjątkiem co najmniej jednego, zostaną zablokowane i pozostaną bezczynne. Innymi słowy, możemy powiedzieć, że bylibyśmy w stanie używać tylko jednego procesu lub wątku w czasie, gdy obowiązuje blokada. Istnieje kilka prostych rozwiązań w celu usunięcia wyżej wymienionych barier -
Ograniczenie udostępniania danych
Najprostszym rozwiązaniem jest nieudostępnianie żadnych zmiennych danych. W takim przypadku nie musimy używać jawnego blokowania, a bariera współbieżności spowodowana wzajemnymi danymi zostałaby rozwiązana.
Pomoc w zakresie struktury danych
Często współbieżne procesy muszą mieć dostęp do tych samych danych w tym samym czasie. Innym rozwiązaniem, niż użycie jawnych blokad, jest użycie struktury danych, która obsługuje współbieżny dostęp. Na przykład możemy użyćqueuemoduł, który zapewnia kolejki bezpieczne dla wątków. Możemy również użyćmultiprocessing.JoinableQueue klasy dla współbieżności opartej na wieloprocesowości.
Niezmienny transfer danych
Czasami struktura danych, której używamy, powiedzmy kolejka współbieżności, nie jest odpowiednia, wtedy możemy przekazać niezmienne dane bez ich blokowania.
Zmienny transfer danych
Kontynuując powyższe rozwiązanie, załóżmy, że jeśli wymagane jest przekazywanie tylko danych podlegających zmianom, a nie danych niezmiennych, to możemy przekazywać zmienne dane, które są tylko do odczytu.
Udostępnianie zasobów we / wy
Inną ważną kwestią we wdrażaniu systemów współbieżnych jest wykorzystanie zasobów we / wy przez wątki lub procesy. Problem pojawia się, gdy jeden wątek lub proces używa wejścia / wyjścia przez tak długi czas, a inny jest bezczynny. Tego rodzaju barierę widzimy pracując z aplikacją wymagającą dużej liczby wejść / wyjść. Można to zrozumieć na przykładzie żądania stron z przeglądarki internetowej. To ciężka aplikacja. Tutaj, jeśli szybkość, z jaką żądane są dane, jest wolniejsza niż szybkość, z jaką są one zużywane, to mamy barierę we / wy w naszym współbieżnym systemie.
Poniższy skrypt Pythona służy do żądania strony internetowej i uzyskania czasu, jaki zajęło naszej sieci uzyskanie żądanej strony -
import urllib.request
import time
ts = time.time()
req = urllib.request.urlopen('http://www.tutorialspoint.com')
pageHtml = req.read()
te = time.time()
print("Page Fetching Time : {} Seconds".format (te-ts))
Po wykonaniu powyższego skryptu możemy uzyskać czas pobierania strony, jak pokazano poniżej.
Wynik
Page Fetching Time: 1.0991398811340332 Seconds
Widzimy, że czas na pobranie strony jest dłuższy niż jedna sekunda. A co, jeśli chcemy pobrać tysiące różnych stron internetowych, możesz zrozumieć, ile czasu zajmie nasza sieć.
Co to jest równoległość?
Równoległość można zdefiniować jako sztukę dzielenia zadań na podzadania, które mogą być przetwarzane jednocześnie. Jest to przeciwieństwo współbieżności, jak omówiono powyżej, w której dwa lub więcej zdarzeń zachodzi w tym samym czasie. Możemy to zrozumieć schematycznie; zadanie jest podzielone na kilka podzadań, które mogą być przetwarzane równolegle, w następujący sposób -
Aby uzyskać więcej informacji na temat różnicy między współbieżnością a równoległością, rozważ następujące punkty -
Współbieżne, ale nie równoległe
Aplikacja może być współbieżna, ale nie równoległa, co oznacza, że w tym samym czasie przetwarza więcej niż jedno zadanie, ale zadania nie są dzielone na podzadania.
Równolegle, ale nie równolegle
Aplikacja może być równoległa, ale nie współbieżna, co oznacza, że działa tylko na jednym zadaniu na raz, a zadania podzielone na podzadania mogą być przetwarzane równolegle.
Ani równoległe, ani współbieżne
Aplikacja nie może być ani równoległa, ani współbieżna. Oznacza to, że działa tylko na jednym zadaniu naraz, a zadanie nigdy nie jest dzielone na podzadania.
Zarówno równoległe, jak i współbieżne
Aplikacja może być zarówno równoległa, jak i współbieżna, co oznacza, że działa jednocześnie na wielu zadaniach, a zadanie jest podzielone na podzadania do wykonywania ich równolegle.
Konieczność równoległości
Możemy osiągnąć paralelizm, rozdzielając podzadania na różne rdzenie pojedynczego procesora lub na wiele komputerów połączonych w sieci.
Rozważ następujące ważne punkty, aby zrozumieć, dlaczego konieczne jest osiągnięcie równoległości:
Wydajne wykonanie kodu
Za pomocą równoległości możemy wydajnie uruchamiać nasz kod. Zaoszczędzi to nasz czas, ponieważ ten sam kod w częściach działa równolegle.
Szybszy niż przetwarzanie sekwencyjne
Obliczenia sekwencyjne są ograniczone czynnikami fizycznymi i praktycznymi, przez co nie jest możliwe uzyskanie szybszych wyników obliczeń. Z drugiej strony ten problem jest rozwiązany przez obliczenia równoległe i daje nam szybsze wyniki obliczeń niż obliczenia sekwencyjne.
Krótszy czas realizacji
Przetwarzanie równoległe skraca czas wykonywania kodu programu.
Jeśli mówimy o prawdziwym przykładzie równoległości, karta graficzna naszego komputera jest przykładem, który podkreśla prawdziwą moc przetwarzania równoległego, ponieważ ma setki indywidualnych rdzeni przetwarzających, które działają niezależnie i mogą wykonywać operacje w tym samym czasie. Z tego powodu jesteśmy w stanie uruchamiać również aplikacje i gry z najwyższej półki.
Zrozumienie procesorów do wdrożenia
Wiemy o współbieżności, równoległości i różnicy między nimi, ale co z systemem, w którym ma być zaimplementowana. Niezbędna jest znajomość systemu, na którym będziemy wdrażać, ponieważ daje nam to możliwość podejmowania świadomych decyzji podczas projektowania oprogramowania. Mamy dwa rodzaje procesorów -
Procesory jednordzeniowe
Procesory jednordzeniowe mogą w dowolnym momencie wykonywać jeden wątek. Te procesory używającontext switchingaby przechowywać wszystkie niezbędne informacje dotyczące wątku w określonym czasie, a następnie przywrócić je później. Mechanizm przełączania kontekstów pomaga nam robić postępy w wielu wątkach w ciągu danej sekundy i wygląda na to, że system pracuje nad wieloma rzeczami.
Procesory jednordzeniowe mają wiele zalet. Te procesory wymagają mniej energii i nie ma złożonego protokołu komunikacyjnego między wieloma rdzeniami. Z drugiej strony szybkość procesorów jednordzeniowych jest ograniczona i nie nadaje się do większych aplikacji.
Procesory wielordzeniowe
Procesory wielordzeniowe mają wiele niezależnych jednostek przetwarzających zwanych również cores.
Takie procesory nie potrzebują mechanizmu przełączania kontekstu, ponieważ każdy rdzeń zawiera wszystko, czego potrzebuje do wykonania sekwencji zapisanych instrukcji.
Cykl pobierania, dekodowania i wykonywania
Rdzenie procesorów wielordzeniowych wykonują cykl. Ten cykl nazywa sięFetch-Decode-Executecykl. Obejmuje następujące kroki -
Sprowadzać
Jest to pierwszy krok cyklu, który polega na pobieraniu instrukcji z pamięci programu.
Rozszyfrować
Niedawno pobrane instrukcje byłyby konwertowane na serię sygnałów, które będą wyzwalać inne części procesora.
Wykonać
Jest to ostatni krok, w którym zostaną wykonane pobrane i zdekodowane instrukcje. Wynik wykonania zostanie zapisany w rejestrze procesora.
Jedną z zalet jest to, że wykonanie w procesorach wielordzeniowych jest szybsze niż w procesorach jednordzeniowych. Nadaje się do większych zastosowań. Z drugiej strony problemem jest złożony protokół komunikacyjny między wieloma rdzeniami. Wiele rdzeni wymaga więcej mocy niż procesory jednordzeniowe.
Podczas projektowania programu lub systemu współbieżnego należy wziąć pod uwagę różne style systemu i architektury pamięci. Jest to bardzo konieczne, ponieważ jeden styl systemu i pamięci może być odpowiedni do jednego zadania, ale może być podatny na błędy w przypadku innego zadania.
Architektury systemów komputerowych obsługujące współbieżność
Michael Flynn w 1972 roku przedstawił taksonomię kategoryzacji różnych stylów architektury systemów komputerowych. Ta taksonomia definiuje cztery różne style w następujący sposób -
- Pojedynczy strumień instrukcji, pojedynczy strumień danych (SISD)
- Pojedynczy strumień instrukcji, wiele strumieni danych (SIMD)
- Wiele strumieni instrukcji, pojedynczy strumień danych (MISD)
- Wiele strumieni instrukcji, wiele strumieni danych (MIMD).
Pojedynczy strumień instrukcji, pojedynczy strumień danych (SISD)
Jak sama nazwa wskazuje, takie systemy miałyby jeden sekwencyjny przychodzący strumień danych i jedną jednostkę przetwarzającą do realizacji strumienia danych. Są jak systemy jednoprocesorowe o równoległej architekturze obliczeniowej. Oto architektura SISD -
Zalety SISD
Zalety architektury SISD są następujące -
- Wymaga mniej energii.
- Nie ma problemu ze złożonym protokołem komunikacyjnym między wieloma rdzeniami.
Wady SISD
Wady architektury SISD są następujące -
- Szybkość architektury SISD jest ograniczona, podobnie jak w przypadku procesorów jednordzeniowych.
- Nie nadaje się do większych zastosowań.
Pojedynczy strumień instrukcji, wiele strumieni danych (SIMD)
Jak sama nazwa wskazuje, tego rodzaju systemy miałyby wiele przychodzących strumieni danych i liczbę jednostek przetwarzających, które mogą działać na pojedynczą instrukcję w danym momencie. Są jak systemy wieloprocesorowe z równoległą architekturą obliczeniową. Oto architektura SIMD -
Najlepszym przykładem SIMD są karty graficzne. Te karty mają setki indywidualnych jednostek przetwarzania. Jeśli mówimy o obliczeniowej różnicy między SISD i SIMD, to o dodawanie tablic[5, 15, 20] i [15, 25, 10],Architektura SISD musiałaby wykonać trzy różne operacje dodawania. Z drugiej strony, dzięki architekturze SIMD, możemy dodać następnie w jednej operacji dodawania.
Zalety SIMD
Zalety architektury SIMD są następujące -
Ta sama operacja na wielu elementach może być wykonana przy użyciu tylko jednej instrukcji.
Wydajność systemu można zwiększyć, zwiększając liczbę rdzeni procesora.
Szybkość przetwarzania jest wyższa niż w architekturze SISD.
Wady SIMD
Wady architektury SIMD są następujące -
- Istnieje złożona komunikacja między liczbą rdzeni procesora.
- Koszt jest wyższy niż w przypadku architektury SISD.
Strumień wielu instrukcji pojedynczych danych (MISD)
Systemy ze strumieniem MISD mają wiele jednostek przetwarzających wykonujących różne operacje, wykonując różne instrukcje na tym samym zestawie danych. Oto architektura MISD -
Przedstawiciele architektury MISD nie istnieją jeszcze komercyjnie.
Strumień danych wielu instrukcji (MIMD)
W systemie wykorzystującym architekturę MIMD każdy procesor w systemie wieloprocesorowym może wykonywać różne zestawy instrukcji niezależnie od różnych zestawów danych równolegle. Jest to przeciwieństwo architektury SIMD, w której pojedyncza operacja jest wykonywana na wielu zestawach danych. Oto architektura MIMD -
Zwykły procesor wieloprocesorowy wykorzystuje architekturę MIMD. Architektury te są zasadniczo używane w wielu obszarach zastosowań, takich jak projektowanie wspomagane komputerowo / produkcja wspomagana komputerowo, symulacja, modelowanie, przełączniki komunikacyjne itp.
Architektury pamięci obsługujące współbieżność
Podczas pracy z takimi pojęciami, jak współbieżność i równoległość, zawsze istnieje potrzeba przyspieszenia programów. Jednym z rozwiązań znalezionych przez projektantów komputerów jest utworzenie wielu komputerów z pamięcią współdzieloną, tj. Komputerów posiadających pojedynczą fizyczną przestrzeń adresową, do której dostęp mają wszystkie rdzenie procesora. W tym scenariuszu może istnieć wiele różnych stylów architektury, ale następujące są trzy ważne style architektoniczne -
UMA (jednolity dostęp do pamięci)
W tym modelu wszystkie procesory równomiernie współdzielą pamięć fizyczną. Wszystkie procesory mają równy czas dostępu do wszystkich słów pamięci. Każdy procesor może mieć prywatną pamięć podręczną. Urządzenia peryferyjne podlegają pewnym regułom.
Gdy wszystkie procesory mają równy dostęp do wszystkich urządzeń peryferyjnych, system nazywa się a symmetric multiprocessor. Gdy tylko jeden lub kilka procesorów ma dostęp do urządzeń peryferyjnych, system nosi nazwęasymmetric multiprocessor.
Niejednolity dostęp do pamięci (NUMA)
W modelu wieloprocesorowym NUMA czas dostępu różni się w zależności od lokalizacji słowa pamięci. Tutaj pamięć współdzielona jest fizycznie rozdzielana między wszystkie procesory, zwane pamięcią lokalną. Zbiór wszystkich lokalnych pamięci tworzy globalną przestrzeń adresową, do której mają dostęp wszystkie procesory.
Architektura pamięci podręcznej (COMA)
Model COMA to wyspecjalizowana wersja modelu NUMA. Tutaj wszystkie rozproszone pamięci główne są konwertowane na pamięci podręczne.
Ogólnie rzecz biorąc, jak wiemy, nić jest bardzo cienkim skręconym sznurkiem, zwykle z bawełny lub jedwabiu, używanym do szycia ubrań i tym podobnych. Ten sam termin wątek jest również używany w świecie programowania komputerowego. A teraz, jak odnosimy się do nici używanej do szycia ubrań i nici używanej do programowania komputerowego? Role pełnione przez oba wątki są tutaj podobne. W ubraniach nić trzyma tkaninę razem, a po drugiej stronie, w programowaniu komputerowym, nić trzyma program komputerowy i pozwala programowi na wykonanie sekwencyjnych działań lub wielu akcji naraz.
Threadto najmniejsza jednostka wykonania w systemie operacyjnym. Sam w sobie nie jest programem, ale działa w programie. Innymi słowy, wątki nie są od siebie niezależne i współdzielą sekcję kodu, sekcję danych itp. Z innymi wątkami. Te wątki są również znane jako lekkie procesy.
Stany wątku
Aby dogłębnie zrozumieć funkcjonalność wątków, musimy poznać cykl życia wątków lub różne stany wątków. Zazwyczaj wątek może istnieć w pięciu różnych stanach. Poniżej przedstawiono różne stany -
Nowy wątek
Nowy wątek rozpoczyna swój cykl życia w nowym stanie. Jednak na tym etapie jeszcze się nie rozpoczął i nie przydzielono mu żadnych środków. Można powiedzieć, że jest to tylko instancja obiektu.
Runnable
Gdy nowo narodzony wątek jest uruchamiany, wątek staje się gotowy do uruchomienia, tj. Czeka na uruchomienie. W tym stanie ma wszystkie zasoby, ale nadal harmonogram zadań nie zaplanował jego uruchomienia.
Bieganie
W tym stanie wątek wykonuje postęp i wykonuje zadanie, które zostało wybrane do uruchomienia przez harmonogram zadań. Teraz wątek może przejść do stanu martwego lub niedziałającego / oczekującego.
Nie działa / czeka
W tym stanie wątek jest wstrzymywany, ponieważ czeka na odpowiedź jakiegoś żądania we / wy lub czeka na zakończenie wykonywania innego wątku.
Nie żyje
Uruchomiony wątek przechodzi w stan zakończony po zakończeniu zadania lub w inny sposób.
Poniższy diagram przedstawia pełny cykl życia wątku -
Rodzaje wątków
W tej sekcji zobaczymy różne rodzaje nici. Rodzaje opisano poniżej -
Wątki na poziomie użytkownika
To są wątki zarządzane przez użytkowników.
W tym przypadku jądro zarządzania wątkami nie jest świadome istnienia wątków. Biblioteka wątków zawiera kod do tworzenia i niszczenia wątków, przekazywania komunikatów i danych między wątkami, planowania wykonywania wątków oraz zapisywania i przywracania kontekstów wątków. Aplikacja rozpoczyna się od pojedynczego wątku.
Przykłady wątków na poziomie użytkownika to -
- Wątki Java
- Wątki POSIX
Zalety wątków na poziomie użytkownika
Poniżej przedstawiono różne zalety wątków na poziomie użytkownika -
- Przełączanie wątków nie wymaga uprawnień trybu jądra.
- Wątek na poziomie użytkownika może działać w dowolnym systemie operacyjnym.
- Planowanie może być specyficzne dla aplikacji w wątku na poziomie użytkownika.
- Wątki na poziomie użytkownika są szybkie w tworzeniu i zarządzaniu.
Wady wątków na poziomie użytkownika
Poniżej przedstawiono różne wady wątków na poziomie użytkownika -
- W typowym systemie operacyjnym większość wywołań systemowych jest blokowana.
- Aplikacja wielowątkowa nie może korzystać z wieloprocesowości.
Wątki na poziomie jądra
Wątki zarządzane przez system operacyjny działają na jądro, które jest rdzeniem systemu operacyjnego.
W tym przypadku jądro zarządza wątkami. W obszarze aplikacji nie ma kodu zarządzania wątkami. Wątki jądra są obsługiwane bezpośrednio przez system operacyjny. Każda aplikacja może zostać zaprogramowana jako wielowątkowa. Wszystkie wątki w aplikacji są obsługiwane w ramach jednego procesu.
Jądro przechowuje informacje kontekstowe dla całego procesu i dla poszczególnych wątków w procesie. Planowanie przez jądro odbywa się na zasadzie wątku. Kernel wykonuje tworzenie wątków, planowanie i zarządzanie w przestrzeni jądra. Wątki jądra są generalnie wolniejsze do tworzenia i zarządzania niż wątki użytkownika. Przykładami wątków na poziomie jądra są Windows, Solaris.
Zalety wątków na poziomie jądra
Poniżej przedstawiono różne zalety wątków na poziomie jądra -
Kernel może jednocześnie zaplanować wiele wątków z tego samego procesu w wielu procesach.
Jeśli jeden wątek procesu jest zablokowany, jądro może zaplanować inny wątek tego samego procesu.
Same procedury jądra mogą być wielowątkowe.
Wady wątków na poziomie jądra
Wątki jądra są generalnie wolniejsze do tworzenia i zarządzania niż wątki użytkownika.
Przeniesienie kontroli z jednego wątku do drugiego w ramach tego samego procesu wymaga przełączenia trybu na jądro.
Blok kontroli wątku - TCB
Blok kontroli wątków (TCB) można zdefiniować jako strukturę danych w jądrze systemu operacyjnego, która zawiera głównie informacje o wątku. Informacje specyficzne dla wątku przechowywane w TCB mogą uwydatnić kilka ważnych informacji o każdym procesie.
Rozważ następujące punkty związane z wątkami zawartymi w TCB -
Thread identification - Jest to unikalny identyfikator wątku (tid) przypisywany do każdego nowego wątku.
Thread state - Zawiera informacje związane ze stanem (uruchomiony, działający, niedziałający, martwy) wątku.
Program Counter (PC) - Wskazuje na aktualną instrukcję programu wątku.
Register set - Zawiera wartości rejestrów wątku przypisane im do obliczeń.
Stack Pointer- Wskazuje na stos wątku w procesie. Zawiera zmienne lokalne w zakresie wątku.
Pointer to PCB - Zawiera wskaźnik do procesu, który utworzył ten wątek.
Relacja między procesem a wątkiem
W wielowątkowości proces i wątek to dwa bardzo ściśle powiązane terminy, których celem jest umożliwienie komputerowi wykonywania więcej niż jednej rzeczy naraz. Proces może zawierać jeden lub więcej wątków, ale wręcz przeciwnie, wątek nie może zawierać procesu. Jednak obie pozostają dwiema podstawowymi jednostkami egzekucyjnymi. Program wykonujący serię instrukcji inicjuje proces i wątkuje oba.
Poniższa tabela przedstawia porównanie procesu i wątku -
Proces | Wątek |
---|---|
Proces jest ciężki lub wymaga dużej ilości zasobów. | Wątek jest lekki, co wymaga mniej zasobów niż proces. |
Przełączanie procesów wymaga interakcji z systemem operacyjnym. | Przełączanie wątków nie wymaga interakcji z systemem operacyjnym. |
W wielu środowiskach przetwarzania każdy proces wykonuje ten sam kod, ale ma własną pamięć i zasoby plikowe. | Wszystkie wątki mogą współdzielić ten sam zestaw otwartych plików, procesów potomnych. |
Jeśli jeden proces jest zablokowany, żaden inny proces nie może zostać wykonany, dopóki pierwszy proces nie zostanie odblokowany. | Podczas gdy jeden wątek jest zablokowany i czeka, można uruchomić drugi wątek w tym samym zadaniu. |
Wiele procesów bez użycia wątków zużywa więcej zasobów. | Wiele procesów wielowątkowych zużywa mniej zasobów. |
W przypadku wielu procesów każdy proces działa niezależnie od innych. | Jeden wątek może czytać, zapisywać lub zmieniać dane innego wątku. |
Jeśli nastąpiłaby jakakolwiek zmiana w procesie nadrzędnym, nie ma to wpływu na procesy potomne. | Jeśli nastąpiłaby jakakolwiek zmiana w głównym wątku, może to wpłynąć na zachowanie innych wątków tego procesu. |
Aby komunikować się z procesami rodzeństwa, procesy muszą wykorzystywać komunikację międzyprocesową. | Wątki mogą bezpośrednio komunikować się z innymi wątkami tego procesu. |
Koncepcja wielowątkowości
Jak omówiliśmy wcześniej, wielowątkowość to zdolność procesora do zarządzania wykorzystaniem systemu operacyjnego poprzez jednoczesne wykonywanie wielu wątków. Główną ideą wielowątkowości jest osiągnięcie równoległości poprzez podzielenie procesu na wiele wątków. Mówiąc prościej, możemy powiedzieć, że wielowątkowość to sposób na osiągnięcie wielozadaniowości przy użyciu pojęcia wątków.
Pojęcie wielowątkowości można zrozumieć na poniższym przykładzie.
Przykład
Załóżmy, że uruchamiamy proces. Proces może polegać na otwarciu słowa MS na napisanie czegoś. W takim procesie jeden wątek zostanie przypisany do otwartego słowa MS, a inny wątek będzie musiał pisać. Teraz załóżmy, że jeśli chcemy coś edytować, to do wykonania zadania edycji będzie potrzebny inny wątek i tak dalej.
Poniższy diagram pomaga nam zrozumieć, jak wiele wątków istnieje w pamięci -
Na powyższym diagramie widać, że w jednym procesie może istnieć więcej niż jeden wątek, w którym każdy wątek zawiera własny zestaw rejestrów i zmienne lokalne. Poza tym wszystkie wątki w procesie mają wspólne zmienne globalne.
Zalety wielowątkowości
Przyjrzyjmy się teraz kilku zaletom wielowątkowości. Zalety są następujące -
Speed of communication - Wielowątkowość poprawia szybkość obliczeń, ponieważ każdy rdzeń lub procesor obsługuje jednocześnie oddzielne wątki.
Program remains responsive - Umożliwia programowi pozostanie responsywnym, ponieważ jeden wątek czeka na dane wejściowe, a inny jednocześnie uruchamia GUI.
Access to global variables - W przypadku wielowątkowości wszystkie wątki danego procesu mają dostęp do zmiennych globalnych, a jeśli nastąpi jakakolwiek zmiana w zmiennej globalnej, będzie ona widoczna również dla innych wątków.
Utilization of resources - Uruchomienie kilku wątków w każdym programie powoduje lepsze wykorzystanie procesora, a czas bezczynności procesora zmniejsza się.
Sharing of data - Nie jest wymagane dodatkowe miejsce na każdy wątek, ponieważ wątki w programie mogą współużytkować te same dane.
Wady wielowątkowości
Przyjrzyjmy się teraz kilku wadom wielowątkowości. Wady są następujące -
Not suitable for single processor system - Wielowątkowość sprawia, że trudno jest osiągnąć wydajność w zakresie szybkości obliczeń w systemie jednoprocesorowym w porównaniu z wydajnością w systemie wieloprocesorowym.
Issue of security - Ponieważ wiemy, że wszystkie wątki w programie współużytkują te same dane, stąd zawsze istnieje kwestia bezpieczeństwa, ponieważ każdy nieznany wątek może zmienić dane.
Increase in complexity - Wielowątkowość może zwiększyć złożoność programu, a debugowanie staje się trudne.
Lead to deadlock state - Wielowątkowość może prowadzić do potencjalnego ryzyka osiągnięcia stanu zakleszczenia przez program.
Synchronization required- Synchronizacja jest wymagana, aby uniknąć wzajemnego wykluczenia. Prowadzi to do większego wykorzystania pamięci i procesora.
W tym rozdziale dowiemy się, jak zaimplementować wątki w Pythonie.
Moduł Pythona do implementacji wątków
Wątki Pythona są czasami nazywane lekkimi procesami, ponieważ wątki zajmują znacznie mniej pamięci niż procesy. Wątki pozwalają na wykonywanie wielu zadań jednocześnie. W Pythonie mamy dwa następujące moduły, które implementują wątki w programie -
<_thread>module
<threading>module
Główna różnica między tymi dwoma modułami polega na tym <_thread> moduł traktuje wątek jako funkcję, podczas gdy <threading>moduł traktuje każdy wątek jako obiekt i implementuje go w sposób obiektowy. Ponadto<_thread>Moduł jest skuteczny w wątkach niskiego poziomu i ma mniej możliwości niż <threading> moduł.
Moduł <_thread>
We wcześniejszej wersji Pythona mieliśmy rozszerzenie <thread>moduł, ale od dłuższego czasu był uważany za „przestarzały”. Zachęcamy użytkowników do korzystania z<threading>moduł zamiast. Dlatego w Pythonie 3 moduł „wątek” nie jest już dostępny. Nazwa została zmieniona na „<_thread>"dla wstecznej niezgodności w Pythonie3.
Aby wygenerować nowy wątek za pomocą <_thread> musimy wywołać moduł start_new_threadmetoda. Działanie tej metody można zrozumieć za pomocą następującej składni -
_thread.start_new_thread ( function, args[, kwargs] )
Tutaj -
args jest krotką argumentów
kwargs jest opcjonalnym słownikiem argumentów słów kluczowych
Jeśli chcemy wywołać funkcję bez przekazywania argumentu, musimy użyć pustej krotki argumentów w args.
To wywołanie metody zwraca natychmiast, uruchamia się wątek potomny i wywołuje funkcję z przekazaną listą argumentów, jeśli istnieje. Wątek kończy się jako i kiedy funkcja zwraca.
Przykład
Poniżej znajduje się przykład generowania nowego wątku przy użyciu <_thread>moduł. Używamy tutaj metody start_new_thread ().
import _thread
import time
def print_time( threadName, delay):
count = 0
while count < 5:
time.sleep(delay)
count += 1
print ("%s: %s" % ( threadName, time.ctime(time.time()) ))
try:
_thread.start_new_thread( print_time, ("Thread-1", 2, ) )
_thread.start_new_thread( print_time, ("Thread-2", 4, ) )
except:
print ("Error: unable to start thread")
while 1:
pass
Wynik
Poniższe dane wyjściowe pomogą nam zrozumieć generowanie nowych wątków bz pomocą <_thread> moduł.
Thread-1: Mon Apr 23 10:03:33 2018
Thread-2: Mon Apr 23 10:03:35 2018
Thread-1: Mon Apr 23 10:03:35 2018
Thread-1: Mon Apr 23 10:03:37 2018
Thread-2: Mon Apr 23 10:03:39 2018
Thread-1: Mon Apr 23 10:03:39 2018
Thread-1: Mon Apr 23 10:03:41 2018
Thread-2: Mon Apr 23 10:03:43 2018
Thread-2: Mon Apr 23 10:03:47 2018
Thread-2: Mon Apr 23 10:03:51 2018
moduł <threading>
Plik <threading>moduł implementuje się w sposób obiektowy i traktuje każdy wątek jako obiekt. W związku z tym zapewnia znacznie wydajniejszą obsługę wysokiego poziomu dla wątków niż moduł <_thread>. Ten moduł jest zawarty w Pythonie 2.4.
Dodatkowe metody w module <threading>
Plik <threading> moduł zawiera wszystkie metody <_thread>moduł, ale zapewnia również dodatkowe metody. Dodatkowe metody są następujące -
threading.activeCount() - Ta metoda zwraca liczbę aktywnych obiektów wątku
threading.currentThread() - Ta metoda zwraca liczbę obiektów wątku w kontrolce wątku wywołującego.
threading.enumerate() - Ta metoda zwraca listę wszystkich aktualnie aktywnych obiektów wątku.
run() - Metoda run () jest punktem wejścia dla wątku.
start() - Metoda start () uruchamia wątek przez wywołanie metody run.
join([time]) - Join () czeka na zakończenie wątków.
isAlive() - Metoda isAlive () sprawdza, czy wątek nadal jest wykonywany.
getName() - Metoda getName () zwraca nazwę wątku.
setName() - Metoda setName () ustawia nazwę wątku.
Do implementacji wątków <threading> moduł ma Thread klasa, która udostępnia następujące metody -
Jak tworzyć wątki za pomocą modułu <threading>?
W tej sekcji dowiemy się, jak tworzyć wątki za pomocą <threading>moduł. Wykonaj poniższe czynności, aby utworzyć nowy wątek za pomocą modułu <threading> -
Step 1 - W tym kroku musimy zdefiniować nową podklasę klasy Thread klasa.
Step 2 - Następnie, aby dodać dodatkowe argumenty, musimy zastąpić __init__(self [,args]) metoda.
Step 3 - W tym kroku musimy nadpisać metodę run (self [, args]), aby zaimplementować to, co wątek powinien zrobić po uruchomieniu.
Teraz, po utworzeniu nowego Thread podklasy, możemy utworzyć jej instancję, a następnie rozpocząć nowy wątek, wywołując plik start(), co z kolei wywołuje run() metoda.
Przykład
Rozważ ten przykład, aby dowiedzieć się, jak wygenerować nowy wątek przy użyciu <threading> moduł.
import threading
import time
exitFlag = 0
class myThread (threading.Thread):
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
def run(self):
print ("Starting " + self.name)
print_time(self.name, self.counter, 5)
print ("Exiting " + self.name)
def print_time(threadName, delay, counter):
while counter:
if exitFlag:
threadName.exit()
time.sleep(delay)
print ("%s: %s" % (threadName, time.ctime(time.time())))
counter -= 1
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print ("Exiting Main Thread")
Starting Thread-1
Starting Thread-2
Wynik
Rozważmy teraz następujący wynik -
Thread-1: Mon Apr 23 10:52:09 2018
Thread-1: Mon Apr 23 10:52:10 2018
Thread-2: Mon Apr 23 10:52:10 2018
Thread-1: Mon Apr 23 10:52:11 2018
Thread-1: Mon Apr 23 10:52:12 2018
Thread-2: Mon Apr 23 10:52:12 2018
Thread-1: Mon Apr 23 10:52:13 2018
Exiting Thread-1
Thread-2: Mon Apr 23 10:52:14 2018
Thread-2: Mon Apr 23 10:52:16 2018
Thread-2: Mon Apr 23 10:52:18 2018
Exiting Thread-2
Exiting Main Thread
Program w Pythonie dla różnych stanów wątków
Istnieje pięć stanów wątku - nowy, gotowy do uruchomienia, uruchomiony, oczekujący i martwy. Spośród tych pięciu Z tych pięciu skupimy się głównie na trzech stanach - biegnących, oczekujących i martwych. Wątek pobiera swoje zasoby w stanie uruchomionym, czeka na zasoby w stanie oczekiwania; ostateczne wydanie zasobu, jeżeli wykonywane i nabyte jest w stanie martwym.
Poniższy program w Pythonie za pomocą metod start (), sleep () i join () pokaże, w jaki sposób wątek wszedł odpowiednio w stan pracy, oczekiwania i stanu martwego.
Step 1 - Zaimportuj niezbędne moduły, <wątek> i <czas>
import threading
import time
Step 2 - Zdefiniuj funkcję, która zostanie wywołana podczas tworzenia wątku.
def thread_states():
print("Thread entered in running state")
Step 3 - Używamy metody sleep () modułu czasu, aby nasz wątek czekał powiedzmy 2 sekundy.
time.sleep(2)
Step 4 - Teraz tworzymy wątek o nazwie T1, który przyjmuje argument funkcji zdefiniowanej powyżej.
T1 = threading.Thread(target=thread_states)
Step 5- Teraz za pomocą funkcji start () możemy uruchomić nasz wątek. Wyprodukuje komunikat, który został przez nas ustawiony podczas definiowania funkcji.
T1.start()
Thread entered in running state
Step 6 - Teraz w końcu możemy zabić wątek metodą join () po zakończeniu jej wykonywania.
T1.join()
Uruchamianie wątku w Pythonie
W pythonie możemy rozpocząć nowy wątek na różne sposoby, ale najłatwiejszym z nich jest zdefiniowanie go jako pojedynczej funkcji. Po zdefiniowaniu funkcji możemy przekazać to jako cel dla nowegothreading.Threadobiekt i tak dalej. Wykonaj następujący kod w języku Python, aby zrozumieć, jak działa funkcja -
import threading
import time
import random
def Thread_execution(i):
print("Execution of Thread {} started\n".format(i))
sleepTime = random.randint(1,4)
time.sleep(sleepTime)
print("Execution of Thread {} finished".format(i))
for i in range(4):
thread = threading.Thread(target=Thread_execution, args=(i,))
thread.start()
print("Active Threads:" , threading.enumerate())
Wynik
Execution of Thread 0 started
Active Threads:
[<_MainThread(MainThread, started 6040)>,
<HistorySavingThread(IPythonHistorySavingThread, started 5968)>,
<Thread(Thread-3576, started 3932)>]
Execution of Thread 1 started
Active Threads:
[<_MainThread(MainThread, started 6040)>,
<HistorySavingThread(IPythonHistorySavingThread, started 5968)>,
<Thread(Thread-3576, started 3932)>,
<Thread(Thread-3577, started 3080)>]
Execution of Thread 2 started
Active Threads:
[<_MainThread(MainThread, started 6040)>,
<HistorySavingThread(IPythonHistorySavingThread, started 5968)>,
<Thread(Thread-3576, started 3932)>,
<Thread(Thread-3577, started 3080)>,
<Thread(Thread-3578, started 2268)>]
Execution of Thread 3 started
Active Threads:
[<_MainThread(MainThread, started 6040)>,
<HistorySavingThread(IPythonHistorySavingThread, started 5968)>,
<Thread(Thread-3576, started 3932)>,
<Thread(Thread-3577, started 3080)>,
<Thread(Thread-3578, started 2268)>,
<Thread(Thread-3579, started 4520)>]
Execution of Thread 0 finished
Execution of Thread 1 finished
Execution of Thread 2 finished
Execution of Thread 3 finished
Wątki demona w Pythonie
Przed zaimplementowaniem wątków demonów w Pythonie musimy wiedzieć o wątkach demonów i ich zastosowaniu. Jeśli chodzi o przetwarzanie danych, demon jest procesem w tle, który obsługuje żądania różnych usług, takich jak wysyłanie danych, przesyłanie plików itp. Byłby uśpiony, gdyby nie był już potrzebny. To samo zadanie można wykonać również z pomocą wątków innych niż demonowe. Jednak w tym przypadku główny wątek musi ręcznie śledzić wątki niebędące demonami. Z drugiej strony, jeśli używamy wątków demonów, główny wątek może o tym całkowicie zapomnieć i zostanie zabity, gdy główny wątek zostanie zamknięty. Inną ważną kwestią dotyczącą wątków demonów jest to, że możemy zdecydować się na użycie ich tylko do nieistotnych zadań, które nie wpłyną na nas, jeśli nie zostaną ukończone lub zostaną zabite w międzyczasie. Poniżej znajduje się implementacja wątków demona w Pythonie -
import threading
import time
def nondaemonThread():
print("starting my thread")
time.sleep(8)
print("ending my thread")
def daemonThread():
while True:
print("Hello")
time.sleep(2)
if __name__ == '__main__':
nondaemonThread = threading.Thread(target = nondaemonThread)
daemonThread = threading.Thread(target = daemonThread)
daemonThread.setDaemon(True)
daemonThread.start()
nondaemonThread.start()
W powyższym kodzie są dwie funkcje, a mianowicie >nondaemonThread() i >daemonThread(). Pierwsza funkcja drukuje swój stan i usypia po 8 sekundach, podczas gdy funkcja deamonThread () drukuje Hello po każdych 2 sekundach przez czas nieokreślony. Możemy zrozumieć różnicę między wątkami nondaemon i daemon za pomocą następujących danych wyjściowych -
Hello
starting my thread
Hello
Hello
Hello
Hello
ending my thread
Hello
Hello
Hello
Hello
Hello
Synchronizację wątków można zdefiniować jako metodę, za pomocą której możemy być pewni, że dwa lub więcej współbieżnych wątków nie uzyskuje jednocześnie dostępu do segmentu programu znanego jako sekcja krytyczna. Z drugiej strony, jak wiemy, ta sekcja krytyczna to część programu, w której uzyskuje się dostęp do współdzielonego zasobu. Dlatego możemy powiedzieć, że synchronizacja jest procesem zapewniającym, że dwa lub więcej wątków nie będzie ze sobą współpracować, uzyskując dostęp do zasobów w tym samym czasie. Poniższy diagram pokazuje, że cztery wątki próbują uzyskać dostęp do krytycznej sekcji programu w tym samym czasie.
Aby było jaśniej, załóżmy, że dwa lub więcej wątków próbuje jednocześnie dodać obiekt do listy. Ta czynność nie może doprowadzić do pomyślnego zakończenia, ponieważ spowoduje usunięcie jednego lub wszystkich obiektów lub całkowicie zepsuje stan listy. Tutaj rola synchronizacji polega na tym, że tylko jeden wątek na raz ma dostęp do listy.
Problemy z synchronizacją wątków
Możemy napotkać problemy podczas wdrażania programowania współbieżnego lub stosowania synchronizujących elementów podstawowych. W tej sekcji omówimy dwie główne kwestie. Problemy są -
- Deadlock
- Warunki wyścigu
Warunki wyścigu
Jest to jeden z głównych problemów w programowaniu współbieżnym. Jednoczesny dostęp do współdzielonych zasobów może prowadzić do wyścigu. Stan wyścigu można zdefiniować jako wystąpienie warunku, w którym dwa lub więcej wątków mogą uzyskać dostęp do udostępnionych danych, a następnie próbować jednocześnie zmienić ich wartość. Z tego powodu wartości zmiennych mogą być nieprzewidywalne i różnić się w zależności od czasów przełączania kontekstu procesów.
Przykład
Rozważ ten przykład, aby zrozumieć pojęcie warunków wyścigu -
Step 1 - W tym kroku musimy zaimportować moduł Threading -
import threading
Step 2 - Teraz zdefiniuj zmienną globalną, powiedzmy x, wraz z jej wartością jako 0 -
x = 0
Step 3 - Teraz musimy zdefiniować increment_global() funkcja, która dokona przyrostu o 1 w tej globalnej funkcji x -
def increment_global():
global x
x += 1
Step 4 - W tym kroku zdefiniujemy plik taskofThread()funkcja, która wywoła funkcję Increment_global () określoną liczbę razy; dla naszego przykładu jest to 50000 razy -
def taskofThread():
for _ in range(50000):
increment_global()
Step 5- Teraz zdefiniuj funkcję main (), w której tworzone są wątki t1 i t2. Oba zostaną uruchomione za pomocą funkcji start () i zaczekają, aż zakończą swoje zadanie za pomocą funkcji join ().
def main():
global x
x = 0
t1 = threading.Thread(target= taskofThread)
t2 = threading.Thread(target= taskofThread)
t1.start()
t2.start()
t1.join()
t2.join()
Step 6- Teraz musimy podać zakres, dla ilu iteracji chcemy wywołać funkcję main (). Tutaj dzwonimy 5 razy.
if __name__ == "__main__":
for i in range(5):
main()
print("x = {1} after Iteration {0}".format(i,x))
Na poniższym wyjściu możemy zobaczyć efekt wyścigu, ponieważ wartość x po każdej iteracji jest spodziewana jako 100000. Jednak istnieje wiele odchyleń wartości. Wynika to z równoczesnego dostępu wątków do wspólnej zmiennej globalnej x.
Wynik
x = 100000 after Iteration 0
x = 54034 after Iteration 1
x = 80230 after Iteration 2
x = 93602 after Iteration 3
x = 93289 after Iteration 4
Radzenie sobie z sytuacją wyścigu za pomocą blokad
Jak widzieliśmy wpływ stanu wyścigu w powyższym programie, potrzebujemy narzędzia do synchronizacji, które poradzi sobie z sytuacją wyścigu między wieloma wątkami. W Pythonie<threading>Moduł zapewnia klasę Lock do radzenia sobie z sytuacją wyścigu. PonadtoLockclass udostępnia różne metody, za pomocą których możemy obsłużyć wyścig między wieloma wątkami. Metody opisano poniżej -
metoda nabycia ()
Ta metoda służy do pozyskiwania, czyli blokowania zamka. Blokada może być blokująca lub nieblokująca w zależności od następującej wartości true lub false -
With value set to True - Jeśli metoda acquiringu () zostanie wywołana z wartością True, która jest domyślnym argumentem, wówczas wykonanie wątku jest blokowane do momentu odblokowania blokady.
With value set to False - Jeśli metoda acquiringu () zostanie wywołana z wartością False, która nie jest argumentem domyślnym, wówczas wykonanie wątku nie jest blokowane, dopóki nie zostanie ustawione na wartość true, tj. Dopóki nie zostanie zablokowane.
release (), metoda
Ta metoda służy do zwolnienia blokady. Poniżej znajduje się kilka ważnych zadań związanych z tą metodą -
Jeśli blokada jest zablokowana, plik release()metoda odblokuje go. Jego zadaniem jest zezwolenie na działanie dokładnie jednego wątku, jeśli więcej niż jeden wątek jest zablokowany i czeka na odblokowanie blokady.
Podniesie ThreadError jeśli zamek jest już odblokowany.
Teraz możemy przepisać powyższy program z klasą lock i jej metodami, aby uniknąć sytuacji wyścigu. Musimy zdefiniować metodę taskofThread () z argumentem lock, a następnie musimy użyć metod pozyskiwania () i release () do blokowania i nieblokowania blokad, aby uniknąć sytuacji wyścigu.
Przykład
Poniżej znajduje się przykład programu w języku Python do zrozumienia koncepcji blokad do radzenia sobie z sytuacją wyścigu -
import threading
x = 0
def increment_global():
global x
x += 1
def taskofThread(lock):
for _ in range(50000):
lock.acquire()
increment_global()
lock.release()
def main():
global x
x = 0
lock = threading.Lock()
t1 = threading.Thread(target = taskofThread, args = (lock,))
t2 = threading.Thread(target = taskofThread, args = (lock,))
t1.start()
t2.start()
t1.join()
t2.join()
if __name__ == "__main__":
for i in range(5):
main()
print("x = {1} after Iteration {0}".format(i,x))
Poniższy wynik pokazuje, że wpływ stanu wyścigu jest pomijany; ponieważ wartość x, po każdej & każdej iteracji, wynosi teraz 100000, co jest zgodne z oczekiwaniami tego programu.
Wynik
x = 100000 after Iteration 0
x = 100000 after Iteration 1
x = 100000 after Iteration 2
x = 100000 after Iteration 3
x = 100000 after Iteration 4
Deadlocks - The Dining Philosophers problem
Impas to kłopotliwa kwestia, z którą można się spotkać podczas projektowania systemów współbieżnych. Możemy zilustrować tę kwestię za pomocą problemu filozofa jadalni w następujący sposób:
Edsger Dijkstra pierwotnie przedstawił problem filozofa kulinarnego, jedną ze słynnych ilustracji jednego z największych problemów systemu współbieżnego zwanego impasem.
W tym problemie jest pięciu znanych filozofów siedzących przy okrągłym stole i jedzących ze swoich misek. Pięciu filozofów może użyć pięciu widelców do jedzenia jedzenia. Jednak filozofowie decydują się na jednoczesne użycie dwóch widelców do jedzenia.
Otóż, są dwa główne warunki dla filozofów. Po pierwsze, każdy z filozofów może być w stanie jedzenia lub myślenia, a po drugie, musi najpierw uzyskać oba rozwidlenia, tj. Lewy i prawy. Problem pojawia się, gdy każdemu z pięciu filozofów w tym samym czasie udaje się wybrać lewą widełkę. Teraz wszyscy czekają, aż właściwy widelec będzie wolny, ale nigdy nie zrezygnują ze swojego widelca, dopóki nie zjedzą jedzenia, a właściwy widelec nigdy nie będzie dostępny. W związku z tym przy stole obiadowym nastąpiłby impas.
Impas w systemie współbieżnym
Teraz, jeśli widzimy, ten sam problem może pojawić się również w naszych współbieżnych systemach. Rozwidlenia w powyższym przykładzie byłyby zasobami systemowymi, a każdy filozof może reprezentować proces, który konkuruje o zasoby.
Rozwiązanie z programem w języku Python
Rozwiązanie tego problemu można znaleźć, dzieląc filozofów na dwa typy - greedy philosophers i generous philosophers. Głównie chciwy filozof będzie próbował chwycić lewy widelec i poczekać, aż się tam znajdzie. Następnie będzie czekał, aż pojawi się właściwy widelec, podniesie go, zje, a następnie odłoży. Z drugiej strony hojny filozof spróbuje podnieść lewy widelec, a jeśli go tam nie ma, zaczeka i spróbuje ponownie za jakiś czas. Jeśli dostaną lewy widelec, spróbują zdobyć prawy. Jeśli dostaną również odpowiedni widelec, zjedzą i zwolnią oba widelce. Jeśli jednak nie dostaną prawego widelca, zwolnią lewy.
Przykład
Poniższy program w Pythonie pomoże nam znaleźć rozwiązanie problemu filozofa kulinarnego -
import threading
import random
import time
class DiningPhilosopher(threading.Thread):
running = True
def __init__(self, xname, Leftfork, Rightfork):
threading.Thread.__init__(self)
self.name = xname
self.Leftfork = Leftfork
self.Rightfork = Rightfork
def run(self):
while(self.running):
time.sleep( random.uniform(3,13))
print ('%s is hungry.' % self.name)
self.dine()
def dine(self):
fork1, fork2 = self.Leftfork, self.Rightfork
while self.running:
fork1.acquire(True)
locked = fork2.acquire(False)
if locked: break
fork1.release()
print ('%s swaps forks' % self.name)
fork1, fork2 = fork2, fork1
else:
return
self.dining()
fork2.release()
fork1.release()
def dining(self):
print ('%s starts eating '% self.name)
time.sleep(random.uniform(1,10))
print ('%s finishes eating and now thinking.' % self.name)
def Dining_Philosophers():
forks = [threading.Lock() for n in range(5)]
philosopherNames = ('1st','2nd','3rd','4th', '5th')
philosophers= [DiningPhilosopher(philosopherNames[i], forks[i%5], forks[(i+1)%5]) \
for i in range(5)]
random.seed()
DiningPhilosopher.running = True
for p in philosophers: p.start()
time.sleep(30)
DiningPhilosopher.running = False
print (" It is finishing.")
Dining_Philosophers()
Powyższy program wykorzystuje koncepcję chciwych i hojnych filozofów. Program wykorzystał również rozszerzenieacquire() i release() metody Lock klasa <threading>moduł. Możemy zobaczyć rozwiązanie na następującym wyjściu -
Wynik
4th is hungry.
4th starts eating
1st is hungry.
1st starts eating
2nd is hungry.
5th is hungry.
3rd is hungry.
1st finishes eating and now thinking.3rd swaps forks
2nd starts eating
4th finishes eating and now thinking.
3rd swaps forks5th starts eating
5th finishes eating and now thinking.
4th is hungry.
4th starts eating
2nd finishes eating and now thinking.
3rd swaps forks
1st is hungry.
1st starts eating
4th finishes eating and now thinking.
3rd starts eating
5th is hungry.
5th swaps forks
1st finishes eating and now thinking.
5th starts eating
2nd is hungry.
2nd swaps forks
4th is hungry.
5th finishes eating and now thinking.
3rd finishes eating and now thinking.
2nd starts eating 4th starts eating
It is finishing.
W prawdziwym życiu, jeśli zespół ludzi pracuje nad wspólnym zadaniem, powinna istnieć między nimi komunikacja w celu prawidłowego wykonania zadania. Ta sama analogia dotyczy również wątków. W programowaniu, aby skrócić idealny czas procesora, tworzymy wiele wątków i przypisujemy różne zadania podrzędne do każdego wątku. Dlatego musi istnieć możliwość komunikacji i powinni oni współdziałać ze sobą, aby zakończyć pracę w zsynchronizowany sposób.
Rozważ następujące ważne punkty związane z komunikacją między wątkami -
No performance gain - Jeśli nie możemy osiągnąć prawidłowej komunikacji między wątkami i procesami, korzyści wydajnościowe wynikające z współbieżności i równoległości są bezużyteczne.
Accomplish task properly - Bez odpowiedniego mechanizmu komunikacji między wątkami przydzielone zadanie nie może zostać poprawnie zakończone.
More efficient than inter-process communication - Komunikacja między wątkami jest wydajniejsza i łatwiejsza w użyciu niż komunikacja między procesami, ponieważ wszystkie wątki w procesie mają tę samą przestrzeń adresową i nie muszą używać pamięci współdzielonej.
Struktury danych Python do bezpiecznej wątkowo komunikacji
W przypadku kodu wielowątkowego pojawia się problem z przekazywaniem informacji z jednego wątku do drugiego. Standardowe prymitywy komunikacyjne nie rozwiązują tego problemu. Dlatego musimy zaimplementować własny obiekt złożony, aby udostępniać obiekty między wątkami, aby komunikacja była bezpieczna dla wątków. Poniżej znajduje się kilka struktur danych, które zapewniają bezpieczną komunikację wątkowo po wprowadzeniu w nich pewnych zmian -
Zestawy
Aby używać struktury danych zestawu w sposób bezpieczny dla wątków, musimy rozszerzyć klasę zestawu, aby zaimplementować własny mechanizm blokujący.
Przykład
Oto przykład rozszerzenia klasy w Pythonie -
class extend_class(set):
def __init__(self, *args, **kwargs):
self._lock = Lock()
super(extend_class, self).__init__(*args, **kwargs)
def add(self, elem):
self._lock.acquire()
try:
super(extend_class, self).add(elem)
finally:
self._lock.release()
def delete(self, elem):
self._lock.acquire()
try:
super(extend_class, self).delete(elem)
finally:
self._lock.release()
W powyższym przykładzie obiekt klasy o nazwie extend_class została zdefiniowana, która jest dalej dziedziczona z Pythona set class. Obiekt lock jest tworzony w konstruktorze tej klasy. Teraz są dwie funkcje -add() i delete(). Te funkcje są zdefiniowane i są bezpieczne dla wątków. Oboje polegają nasuper funkcjonalność klasy z jednym kluczowym wyjątkiem.
Dekorator
Jest to kolejna kluczowa metoda komunikacji bezpiecznej dla wątków, polegająca na użyciu dekoratorów.
Przykład
Rozważmy przykład Pythona, który pokazuje, jak używać dekoratorów & mminus;
def lock_decorator(method):
def new_deco_method(self, *args, **kwargs):
with self._lock:
return method(self, *args, **kwargs)
return new_deco_method
class Decorator_class(set):
def __init__(self, *args, **kwargs):
self._lock = Lock()
super(Decorator_class, self).__init__(*args, **kwargs)
@lock_decorator
def add(self, *args, **kwargs):
return super(Decorator_class, self).add(elem)
@lock_decorator
def delete(self, *args, **kwargs):
return super(Decorator_class, self).delete(elem)
W powyższym przykładzie zdefiniowano metodę dekoratora o nazwie lock_decorator, która jest dalej dziedziczona z klasy metod Pythona. Następnie w konstruktorze tej klasy tworzony jest obiekt blokady. Teraz mamy dwie funkcje - add () i delete (). Te funkcje są zdefiniowane i są bezpieczne dla wątków. Oba polegają na najwyższej klasy funkcjonalności z jednym kluczowym wyjątkiem.
Listy
Struktura danych listy jest bezpieczna dla wątków, szybka i łatwa do tymczasowego przechowywania w pamięci. W Cpython GIL chroni przed jednoczesnym dostępem do nich. Jak dowiedzieliśmy się, że listy są bezpieczne dla wątków, ale co z zawartymi w nich danymi. W rzeczywistości dane listy nie są chronione. Na przykład,L.append(x)nie gwarantuje zwrócenia oczekiwanego wyniku, jeśli inny wątek próbuje zrobić to samo. To dlatego, że chociażappend() jest operacją niepodzielną i bezpieczną dla wątków, ale drugi wątek próbuje jednocześnie modyfikować dane listy, dlatego na wyjściu widać skutki uboczne warunków wyścigu.
Aby rozwiązać tego rodzaju problem i bezpiecznie zmodyfikować dane, musimy zaimplementować odpowiedni mechanizm blokujący, który dodatkowo gwarantuje, że wiele wątków nie może potencjalnie znaleźć się w warunkach wyścigu. Aby zaimplementować odpowiedni mechanizm blokujący, możemy rozszerzyć klasę, tak jak to zrobiliśmy w poprzednich przykładach.
Niektóre inne niepodzielne operacje na listach są następujące -
L.append(x)
L1.extend(L2)
x = L[i]
x = L.pop()
L1[i:j] = L2
L.sort()
x = y
x.field = y
D[x] = y
D1.update(D2)
D.keys()
Tutaj -
- L, L1, L2 to listy
- D, D1, D2 to dykty
- x, y to obiekty
- i, j są ints
Kolejki
Jeśli dane listy nie są chronione, możemy ponieść konsekwencje. Możemy otrzymać lub usunąć błędne dane dotyczące warunków wyścigu. Dlatego zaleca się stosowanie struktury danych kolejki. Rzeczywistym przykładem kolejki może być jednopasmowa droga jednokierunkowa, na której pojazd wjeżdża pierwszy, zjeżdża jako pierwszy. Więcej rzeczywistych przykładów można zobaczyć w kolejkach przy kasach biletowych i na przystankach autobusowych.
Kolejki są domyślnie bezpieczną wątkowo strukturą danych i nie musimy się martwić o implementację złożonego mechanizmu blokowania. Python zapewnia nam
Rodzaje kolejek
W tej sekcji omówimy różne typy kolejek. Python udostępnia trzy opcje kolejek do użycia z<queue> moduł -
- Normalne kolejki (FIFO, pierwsze weszło, pierwsze wyszło)
- LIFO, Last in First Out
- Priority
Dowiemy się o różnych kolejkach w kolejnych sekcjach.
Normalne kolejki (FIFO, pierwsze weszło, pierwsze wyszło)
Jest to najczęściej używana implementacja kolejki oferowana przez Pythona. W tym mechanizmie kolejkowania ten, kto będzie pierwszy, otrzyma usługę jako pierwszy. FIFO jest również nazywane normalnymi kolejkami. Kolejki FIFO można przedstawić w następujący sposób -
Implementacja kolejki FIFO w języku Python
W Pythonie kolejkę FIFO można zaimplementować zarówno z pojedynczym wątkiem, jak iz wielowątkowością.
Kolejka FIFO z pojedynczym wątkiem
Aby zaimplementować kolejkę FIFO z pojedynczym wątkiem, plik Queueclass zaimplementuje podstawowy kontener „pierwszy na wejściu, pierwszy na wyjściu”. Elementy zostaną dodane do jednego „końca” sekwencji za pomocąput()i usunięte z drugiego końca za pomocą get().
Przykład
Poniżej znajduje się program w Pythonie do implementacji kolejki FIFO z pojedynczym wątkiem -
import queue
q = queue.Queue()
for i in range(8):
q.put("item-" + str(i))
while not q.empty():
print (q.get(), end = " ")
Wynik
item-0 item-1 item-2 item-3 item-4 item-5 item-6 item-7
Dane wyjściowe pokazują, że powyższy program używa pojedynczego wątku do zilustrowania, że elementy są usuwane z kolejki w tej samej kolejności, w jakiej są wstawiane.
Kolejka FIFO z wieloma wątkami
Aby zaimplementować FIFO z wieloma wątkami, musimy zdefiniować funkcję myqueue (), która jest rozszerzana z modułu queue. Działanie metod get () i put () jest takie samo, jak omówiono powyżej przy implementacji kolejki FIFO z pojedynczym wątkiem. Następnie, aby był wielowątkowy, musimy zadeklarować i utworzyć instancję wątków. Wątki te będą zużywać kolejkę w sposób FIFO.
Przykład
Poniżej znajduje się program w Pythonie do implementacji kolejki FIFO z wieloma wątkami
import threading
import queue
import random
import time
def myqueue(queue):
while not queue.empty():
item = queue.get()
if item is None:
break
print("{} removed {} from the queue".format(threading.current_thread(), item))
queue.task_done()
time.sleep(2)
q = queue.Queue()
for i in range(5):
q.put(i)
threads = []
for i in range(4):
thread = threading.Thread(target=myqueue, args=(q,))
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
Wynik
<Thread(Thread-3654, started 5044)> removed 0 from the queue
<Thread(Thread-3655, started 3144)> removed 1 from the queue
<Thread(Thread-3656, started 6996)> removed 2 from the queue
<Thread(Thread-3657, started 2672)> removed 3 from the queue
<Thread(Thread-3654, started 5044)> removed 4 from the queue
LIFO, kolejka Last in First Out
Ta kolejka używa zupełnie odwrotnej analogii niż kolejki FIFO (First in First Out). W tym mechanizmie kolejkowania ten, kto jest ostatni, otrzyma usługę jako pierwszy. Jest to podobne do implementacji struktury danych stosu. Kolejki LIFO okazują się przydatne podczas implementacji wyszukiwania w głąb, takich jak algorytmy sztucznej inteligencji.
Implementacja kolejki LIFO w Pythonie
W Pythonie kolejkę LIFO można zaimplementować zarówno z pojedynczym wątkiem, jak iz wielowątkowością.
Kolejka LIFO z pojedynczym wątkiem
Aby zaimplementować kolejkę LIFO z pojedynczym wątkiem, plik Queue class zaimplementuje podstawowy kontener „ostatni na wejściu, pierwszy na wyjściu” przy użyciu struktury Queue.LifoQueue. Teraz dzwonięput(), elementy są dodawane w głowicy pojemnika i usuwane z głowicy również podczas używania get().
Przykład
Poniżej znajduje się program w Pythonie do implementacji kolejki LIFO z pojedynczym wątkiem -
import queue
q = queue.LifoQueue()
for i in range(8):
q.put("item-" + str(i))
while not q.empty():
print (q.get(), end=" ")
Output:
item-7 item-6 item-5 item-4 item-3 item-2 item-1 item-0
Dane wyjściowe pokazują, że powyższy program używa pojedynczego wątku do zilustrowania, że elementy są usuwane z kolejki w odwrotnej kolejności, w jakiej są wstawiane.
Kolejka LIFO z wieloma wątkami
Implementacja jest podobna do implementacji kolejek FIFO z wieloma wątkami. Jedyna różnica polega na tym, że musimy użyć rozszerzeniaQueue klasa, która zaimplementuje podstawowy kontener „ostatni na wejściu, pierwszy na wyjściu” przy użyciu struktury Queue.LifoQueue.
Przykład
Poniżej znajduje się program w Pythonie do implementacji kolejki LIFO z wieloma wątkami -
import threading
import queue
import random
import time
def myqueue(queue):
while not queue.empty():
item = queue.get()
if item is None:
break
print("{} removed {} from the queue".format(threading.current_thread(), item))
queue.task_done()
time.sleep(2)
q = queue.LifoQueue()
for i in range(5):
q.put(i)
threads = []
for i in range(4):
thread = threading.Thread(target=myqueue, args=(q,))
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
Wynik
<Thread(Thread-3882, started 4928)> removed 4 from the queue
<Thread(Thread-3883, started 4364)> removed 3 from the queue
<Thread(Thread-3884, started 6908)> removed 2 from the queue
<Thread(Thread-3885, started 3584)> removed 1 from the queue
<Thread(Thread-3882, started 4928)> removed 0 from the queue
Kolejka priorytetowa
W kolejkach FIFO i LIFO kolejność pozycji jest powiązana z kolejnością wstawiania. Jednak w wielu przypadkach priorytet jest ważniejszy niż kolejność wstawiania. Rozważmy przykład z prawdziwego świata. Załóżmy, że ochrona na lotnisku sprawdza osoby należące do różnych kategorii. Osoby z VVIP, personel linii lotniczych, urzędnik celny, kategorie mogą być sprawdzane na zasadzie pierwszeństwa zamiast sprawdzania na podstawie przylotu, tak jak w przypadku zwykłych ludzi.
Innym ważnym aspektem, który należy wziąć pod uwagę w przypadku kolejki priorytetowej, jest sposób opracowania harmonogramu zadań. Jednym z typowych projektów jest obsługa większości zadań agenta na podstawie priorytetów w kolejce. Ta struktura danych może służyć do pobierania elementów z kolejki na podstawie ich wartości priorytetu.
Implementacja kolejki priorytetów w Pythonie
W Pythonie kolejka priorytetowa może być implementowana zarówno z pojedynczym wątkiem, jak i wielowątkowością.
Kolejka priorytetowa z jednym wątkiem
Aby zaimplementować kolejkę priorytetową z pojedynczym wątkiem, plik Queue class zaimplementuje zadanie na kontenerze priorytetowym przy użyciu struktury Queue.Kolejka priorytetowa. Teraz dzwonięput(), elementy są dodawane z wartością, przy której najniższa wartość będzie miała najwyższy priorytet, a zatem zostanie pobrana jako pierwsza przy użyciu get().
Przykład
Rozważmy następujący program w Pythonie do implementacji kolejki Priority z pojedynczym wątkiem -
import queue as Q
p_queue = Q.PriorityQueue()
p_queue.put((2, 'Urgent'))
p_queue.put((1, 'Most Urgent'))
p_queue.put((10, 'Nothing important'))
prio_queue.put((5, 'Important'))
while not p_queue.empty():
item = p_queue.get()
print('%s - %s' % item)
Wynik
1 – Most Urgent
2 - Urgent
5 - Important
10 – Nothing important
Na powyższym wyjściu widzimy, że kolejka przechowała pozycje w oparciu o priorytet - wartość bez priorytetu ma wysoki priorytet.
Kolejka priorytetowa z wieloma wątkami
Implementacja jest podobna do implementacji kolejek FIFO i LIFO z wieloma wątkami. Jedyna różnica polega na tym, że musimy użyć rozszerzeniaQueue klasa do inicjowania priorytetu przy użyciu struktury Queue.PriorityQueue. Inna różnica dotyczy sposobu generowania kolejki. W poniższym przykładzie zostanie wygenerowany z dwoma identycznymi zestawami danych.
Przykład
Poniższy program w Pythonie pomaga w implementacji kolejki priorytetowej z wieloma wątkami -
import threading
import queue
import random
import time
def myqueue(queue):
while not queue.empty():
item = queue.get()
if item is None:
break
print("{} removed {} from the queue".format(threading.current_thread(), item))
queue.task_done()
time.sleep(1)
q = queue.PriorityQueue()
for i in range(5):
q.put(i,1)
for i in range(5):
q.put(i,1)
threads = []
for i in range(2):
thread = threading.Thread(target=myqueue, args=(q,))
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
Wynik
<Thread(Thread-4939, started 2420)> removed 0 from the queue
<Thread(Thread-4940, started 3284)> removed 0 from the queue
<Thread(Thread-4939, started 2420)> removed 1 from the queue
<Thread(Thread-4940, started 3284)> removed 1 from the queue
<Thread(Thread-4939, started 2420)> removed 2 from the queue
<Thread(Thread-4940, started 3284)> removed 2 from the queue
<Thread(Thread-4939, started 2420)> removed 3 from the queue
<Thread(Thread-4940, started 3284)> removed 3 from the queue
<Thread(Thread-4939, started 2420)> removed 4 from the queue
<Thread(Thread-4940, started 3284)> removed 4 from the queue
W tym rozdziale dowiemy się o testowaniu aplikacji wątkowych. Dowiemy się również, jak ważne jest testowanie.
Dlaczego testować?
Zanim przejdziemy do dyskusji o znaczeniu testowania, musimy wiedzieć, czym jest testowanie. Ogólnie rzecz biorąc, testowanie to technika sprawdzania, jak dobrze coś działa. Z drugiej strony, szczególnie jeśli mówimy o programach komputerowych lub oprogramowaniu, testowanie jest techniką uzyskiwania dostępu do funkcjonalności programu.
W tej sekcji omówimy znaczenie testowania oprogramowania. W przypadku tworzenia oprogramowania przed wydaniem oprogramowania klientowi musi istnieć podwójna kontrola. Dlatego bardzo ważne jest testowanie oprogramowania przez doświadczony zespół testowy. Rozważ następujące punkty, aby zrozumieć znaczenie testowania oprogramowania -
Poprawa jakości oprogramowania
Z pewnością żadna firma nie chce dostarczać oprogramowania niskiej jakości i żaden klient nie chce kupować oprogramowania niskiej jakości. Testowanie poprawia jakość oprogramowania poprzez znajdowanie i naprawianie błędów.
Zadowolenie klientów
Najważniejszą częścią każdej firmy jest zadowolenie klientów. Dostarczając wolne od błędów i dobrej jakości oprogramowanie, firmy mogą osiągnąć satysfakcję klientów.
Zmniejsz wpływ nowych funkcji
Załóżmy, że stworzyliśmy system oprogramowania składający się z 10000 linii i musimy dodać nową funkcję, wtedy zespół programistów miałby obawy o wpływ tej nowej funkcji na całe oprogramowanie. Tutaj również testowanie odgrywa istotną rolę, ponieważ jeśli zespół testowy wykonał dobry zestaw testów, może to uchronić nas przed potencjalnymi katastrofalnymi przerwami.
Doświadczenie użytkownika
Kolejną najważniejszą częścią każdej firmy jest doświadczenie użytkowników tego produktu. Tylko testy mogą zapewnić, że użytkownik końcowy uzna produkt za prosty i łatwy w użyciu.
Zmniejszenie wydatków
Testowanie może obniżyć całkowity koszt oprogramowania poprzez znajdowanie i naprawianie błędów w fazie testowania jego rozwoju, zamiast naprawiania go po dostarczeniu. Jeśli po dostarczeniu oprogramowania wystąpi poważny błąd, zwiększy to jego wymierne koszty, powiedzmy w postaci wydatków i kosztów niematerialnych, np. Niezadowolenie klienta, negatywną reputację firmy itp.
Co przetestować?
Zawsze zaleca się posiadanie odpowiedniej wiedzy na temat tego, co ma być testowane. W tej sekcji najpierw zrozumiemy, jaki jest główny motyw testera podczas testowania dowolnego oprogramowania. Pokrycie kodu, tj. Ile wierszy kodu trafia w nasz zestaw testów podczas testowania, należy unikać. Dzieje się tak dlatego, że podczas testowania skupianie się tylko na liczbie wierszy kodu nie dodaje żadnej wartości do naszego systemu. Mogą pozostać pewne błędy, które będą widoczne później na późniejszym etapie, nawet po wdrożeniu.
Rozważ następujące ważne punkty związane z tym, co przetestować -
Musimy skupić się na testowaniu funkcjonalności kodu, a nie pokryciu kodu.
Najpierw musimy przetestować najważniejsze części kodu, a następnie przejść do mniej ważnych części kodu. Na pewno zaoszczędzi to czas.
Tester musi mieć wiele różnych testów, które mogą doprowadzić oprogramowanie do granic możliwości.
Podejścia do testowania współbieżnych programów
Ze względu na możliwość wykorzystania prawdziwych możliwości architektury wielordzeniowej, współbieżne systemy oprogramowania zastępują systemy sekwencyjne. W ostatnim czasie współbieżne programy systemowe są używane we wszystkim, od telefonów komórkowych po pralki, od samochodów po samoloty itp. Musimy być bardziej ostrożni podczas testowania współbieżnych programów, ponieważ jeśli dodaliśmy wiele wątków do aplikacji jednowątkowej, już błąd, wtedy mielibyśmy wiele błędów.
Techniki testowania współbieżnych programów w dużym stopniu koncentrują się na wyborze przeplotu, który ujawnia potencjalnie szkodliwe wzorce, takie jak warunki wyścigu, zakleszczenia i naruszenie atomowości. Poniżej przedstawiono dwa podejścia do testowania współbieżnych programów -
Systematyczne badanie
Podejście to ma na celu jak najszersze zbadanie przestrzeni przeplotów. Takie podejścia mogą przyjąć technikę brutalnej siły, a inne przyjmują technikę redukcji częściowego rzędu lub technikę heurystyczną do badania przestrzeni przeplotów.
Oparte na własności
Podejścia oparte na właściwościach opierają się na obserwacji, że błędy współbieżności są bardziej prawdopodobne w przypadku przeplotów, które ujawniają określone właściwości, takie jak podejrzany wzorzec dostępu do pamięci. Różne podejścia oparte na właściwościach dotyczą różnych usterek, takich jak warunki wyścigu, zakleszczenia i naruszenie atomowości, co dodatkowo zależy od jednej lub innych określonych właściwości.
Strategie testowania
Strategia testowania jest również znana jako podejście testowe. Strategia definiuje sposób przeprowadzania testów. Podejście testowe obejmuje dwie techniki -
Proaktywne
Podejście, w którym proces projektowania testów jest inicjowany tak wcześnie, jak to możliwe, aby znaleźć i naprawić usterki przed utworzeniem kompilacji.
Reaktywny
Podejście, w którym testowanie rozpoczyna się dopiero po zakończeniu procesu programowania.
Przed zastosowaniem jakiejkolwiek strategii testowej lub podejścia do programu w języku Python, musimy mieć podstawowe pojęcie o rodzaju błędów, jakie może mieć program. Błędy są następujące -
Błędy składniowe
Podczas tworzenia programu może wystąpić wiele drobnych błędów. Błędy są głównie spowodowane błędami pisarskimi. Na przykład brak dwukropka lub nieprawidłowa pisownia słowa kluczowego itp. Takie błędy wynikają z błędu w składni programu, a nie z logiki. Stąd te błędy nazywane są błędami składniowymi.
Błędy semantyczne
Błędy semantyczne nazywane są również błędami logicznymi. Jeśli w programie wystąpi błąd logiczny lub semantyczny, instrukcja skompiluje się i uruchomi poprawnie, ale nie da pożądanego wyniku, ponieważ logika nie jest poprawna.
Testów jednostkowych
Jest to jedna z najczęściej używanych strategii testowania programów w Pythonie. Ta strategia jest używana do testowania jednostek lub komponentów kodu. Przez jednostki lub komponenty rozumiemy klasy lub funkcje kodu. Testowanie jednostkowe upraszcza testowanie dużych systemów programowania poprzez testowanie „małych” jednostek. Przy pomocy powyższej koncepcji testowanie jednostkowe można zdefiniować jako metodę, w której poszczególne jednostki kodu źródłowego są testowane w celu określenia, czy zwracają pożądane wyniki.
W kolejnych sekcjach dowiemy się o różnych modułach Pythona do testowania jednostkowego.
unittest module
Pierwszym modułem do testów jednostkowych jest moduł unittest. Jest inspirowany JUnitem i domyślnie zawarty w Pythonie3.6. Obsługuje automatyzację testów, udostępnianie kodu konfiguracji i zamykania testów, agregację testów w kolekcje oraz niezależność testów od struktury raportowania.
Poniżej znajduje się kilka ważnych koncepcji obsługiwanych przez moduł unittest
Uchwyt tekstowy
Służy do skonfigurowania testu, aby można go było uruchomić przed rozpoczęciem testu i rozebrać po zakończeniu testu. Może wiązać się z utworzeniem tymczasowej bazy danych, katalogów itp. Potrzebnych przed rozpoczęciem testu.
Przypadek testowy
Przypadek testowy sprawdza, czy wymagana odpowiedź pochodzi z określonego zestawu danych wejściowych, czy nie. Moduł unittest zawiera klasę bazową o nazwie TestCase, której można użyć do tworzenia nowych przypadków testowych. Zawiera dwie domyślne metody -
setUp()- metoda hakowa do ustawiania osprzętu badawczego przed jej wykonaniem. Jest to wywoływane przed wywołaniem zaimplementowanych metod testowych.
tearDown( - metoda haka do dekonstrukcji urządzenia klasy po uruchomieniu wszystkich testów w klasie.
Zestaw testów
Jest to zbiór zestawów testów, przypadków testowych lub obu.
Biegacz testowy
Kontroluje uruchamianie przypadków testowych lub garniturów i dostarcza wynik użytkownikowi. Może używać graficznego interfejsu użytkownika lub prostego interfejsu tekstowego w celu zapewnienia wyniku.
Example
Poniższy program w języku Python używa modułu unittest do testowania modułu o nazwie Fibonacci. Program pomaga w obliczaniu szeregu Fibonacciego liczby. W tym przykładzie utworzyliśmy klasę o nazwie Fibo_test, aby zdefiniować przypadki testowe przy użyciu różnych metod. Te metody są dziedziczone z unittest.TestCase. Domyślnie używamy dwóch metod - setUp () i tearDown (). Definiujemy również metodę testfibocal. Nazwa testu musi zaczynać się od testu literowego. W ostatnim bloku unittest.main () udostępnia interfejs wiersza poleceń do skryptu testowego.
import unittest
def fibonacci(n):
a, b = 0, 1
for i in range(n):
a, b = b, a + b
return a
class Fibo_Test(unittest.TestCase):
def setUp(self):
print("This is run before our tests would be executed")
def tearDown(self):
print("This is run after the completion of execution of our tests")
def testfibocal(self):
self.assertEqual(fib(0), 0)
self.assertEqual(fib(1), 1)
self.assertEqual(fib(5), 5)
self.assertEqual(fib(10), 55)
self.assertEqual(fib(20), 6765)
if __name__ == "__main__":
unittest.main()
Po uruchomieniu z wiersza poleceń powyższy skrypt generuje dane wyjściowe, które wyglądają następująco:
Wynik
This runs before our tests would be executed.
This runs after the completion of execution of our tests.
.
----------------------------------------------------------------------
Ran 1 test in 0.006s
OK
Teraz, żeby było jaśniej, zmieniamy nasz kod, który pomógł w zdefiniowaniu modułu Fibonacciego.
Rozważmy następujący blok kodu jako przykład -
def fibonacci(n):
a, b = 0, 1
for i in range(n):
a, b = b, a + b
return a
Wprowadzono kilka zmian w bloku kodu, jak pokazano poniżej -
def fibonacci(n):
a, b = 1, 1
for i in range(n):
a, b = b, a + b
return a
Teraz, po uruchomieniu skryptu ze zmienionym kodem, otrzymamy następujący wynik -
This runs before our tests would be executed.
This runs after the completion of execution of our tests.
F
======================================================================
FAIL: testCalculation (__main__.Fibo_Test)
----------------------------------------------------------------------
Traceback (most recent call last):
File "unitg.py", line 15, in testCalculation
self.assertEqual(fib(0), 0)
AssertionError: 1 != 0
----------------------------------------------------------------------
Ran 1 test in 0.007s
FAILED (failures = 1)
Powyższe dane wyjściowe pokazują, że moduł nie dał żądanego wyjścia.
Moduł Docktest
Moduł docktest pomaga również w testowaniu jednostkowym. Jest również dostarczany w pakiecie z pythonem. Jest łatwiejszy w użyciu niż moduł unittest. Moduł unittest jest bardziej odpowiedni do złożonych testów. Aby skorzystać z modułu doctest, musimy go zaimportować. Dokumentacja odpowiedniej funkcji musi mieć interaktywną sesję Pythona wraz z wynikami.
Jeśli w naszym kodzie wszystko jest w porządku, moduł docktest nie będzie wychodził; w przeciwnym razie dostarczy dane wyjściowe.
Przykład
Poniższy przykład Pythona używa modułu docktest do testowania modułu o nazwie Fibonacci, który pomaga w obliczaniu szeregu Fibonacciego liczby.
import doctest
def fibonacci(n):
"""
Calculates the Fibonacci number
>>> fibonacci(0)
0
>>> fibonacci(1)
1
>>> fibonacci(10)
55
>>> fibonacci(20)
6765
>>>
"""
a, b = 1, 1
for i in range(n):
a, b = b, a + b
return a
if __name__ == "__main__":
doctest.testmod()
Widzimy, że ciąg dokumentacyjny odpowiedniej funkcji o nazwie fib zawierał interaktywną sesję Pythona wraz z wynikami. Jeśli nasz kod jest w porządku, moduł doctest nie wyświetli żadnych danych wyjściowych. Ale żeby zobaczyć, jak to działa, możemy uruchomić go z opcją –v.
(base) D:\ProgramData>python dock_test.py -v
Trying:
fibonacci(0)
Expecting:
0
ok
Trying:
fibonacci(1)
Expecting:
1
ok
Trying:
fibonacci(10)
Expecting:
55
ok
Trying:
fibonacci(20)
Expecting:
6765
ok
1 items had no tests:
__main__
1 items passed all tests:
4 tests in __main__.fibonacci
4 tests in 2 items.
4 passed and 0 failed.
Test passed.
Teraz zmienimy kod, który pomógł w zdefiniowaniu modułu Fibonacciego
Rozważmy następujący blok kodu jako przykład -
def fibonacci(n):
a, b = 0, 1
for i in range(n):
a, b = b, a + b
return a
Poniższy blok kodu pomaga w zmianach -
def fibonacci(n):
a, b = 1, 1
for i in range(n):
a, b = b, a + b
return a
Po uruchomieniu skryptu nawet bez opcji –v, ze zmienionym kodem, otrzymamy dane wyjściowe, jak pokazano poniżej.
Wynik
(base) D:\ProgramData>python dock_test.py
**********************************************************************
File "unitg.py", line 6, in __main__.fibonacci
Failed example:
fibonacci(0)
Expected:
0
Got:
1
**********************************************************************
File "unitg.py", line 10, in __main__.fibonacci
Failed example:
fibonacci(10)
Expected:
55
Got:
89
**********************************************************************
File "unitg.py", line 12, in __main__.fibonacci
Failed example:
fibonacci(20)
Expected:
6765
Got:
10946
**********************************************************************
1 items had failures:
3 of 4 in __main__.fibonacci
***Test Failed*** 3 failures.
Na powyższym wyjściu widać, że trzy testy zakończyły się niepowodzeniem.
W tym rozdziale dowiemy się, jak debugować aplikacje wątku. Dowiemy się również, jak ważne jest debugowanie.
Co to jest debugowanie?
W programowaniu komputerowym debugowanie to proces znajdowania i usuwania błędów, błędów i nieprawidłowości w programie komputerowym. Proces ten rozpoczyna się natychmiast po napisaniu kodu i jest kontynuowany w kolejnych etapach, gdy kod jest łączony z innymi jednostkami programowania w celu utworzenia produktu programowego. Debugowanie jest częścią procesu testowania oprogramowania i stanowi integralną część całego cyklu życia oprogramowania.
Python Debugger
Debuger języka Python lub pdbjest częścią standardowej biblioteki Pythona. Jest to dobre narzędzie zastępcze do śledzenia trudnych do znalezienia błędów i pozwala nam szybko i niezawodnie naprawić wadliwy kod. Oto dwa najważniejsze zadaniapdp debugger -
- Pozwala nam to sprawdzić wartości zmiennych w czasie wykonywania.
- Możemy przejść przez kod i ustawić również punkty przerwania.
Możemy pracować z pdb na dwa sposoby -
- Za pomocą wiersza poleceń; Nazywa się to również debugowaniem pośmiertnym.
- Poprzez interaktywne uruchamianie pdb.
Praca z pdb
Do pracy z debugerem Pythona musimy użyć następującego kodu w miejscu, w którym chcemy się włamać do debuggera -
import pdb;
pdb.set_trace()
Rozważ następujące polecenia, aby pracować z pdb za pośrednictwem wiersza poleceń.
- h(help)
- d(down)
- u(up)
- b(break)
- cl(clear)
- l(list))
- n(next))
- c(continue)
- s(step)
- r(return))
- b(break)
Poniżej znajduje się demonstracja polecenia h (pomoc) debuggera języka Python -
import pdb
pdb.set_trace()
--Call--
>d:\programdata\lib\site-packages\ipython\core\displayhook.py(247)__call__()
-> def __call__(self, result = None):
(Pdb) h
Documented commands (type help <topic>):
========================================
EOF c d h list q rv undisplay
a cl debug help ll quit s unt
alias clear disable ignore longlist r source until
args commands display interact n restart step up
b condition down j next return tbreak w
break cont enable jump p retval u whatis
bt continue exit l pp run unalias where
Miscellaneous help topics:
==========================
exec pdb
Przykład
Pracując z debugerem Pythona, możemy ustawić punkt przerwania w dowolnym miejscu skryptu za pomocą następujących linii -
import pdb;
pdb.set_trace()
Po ustawieniu punktu przerwania możemy normalnie uruchomić skrypt. Skrypt będzie wykonywany do pewnego momentu; do miejsca, w którym linia została ustawiona. Rozważmy następujący przykład, w którym uruchomimy skrypt, używając wyżej wymienionych linii w różnych miejscach skryptu -
import pdb;
a = "aaa"
pdb.set_trace()
b = "bbb"
c = "ccc"
final = a + b + c
print (final)
Kiedy powyższy skrypt zostanie uruchomiony, będzie wykonywał program do a = „aaa”, możemy to sprawdzić w następującym wyniku.
Wynik
--Return--
> <ipython-input-7-8a7d1b5cc854>(3)<module>()->None
-> pdb.set_trace()
(Pdb) p a
'aaa'
(Pdb) p b
*** NameError: name 'b' is not defined
(Pdb) p c
*** NameError: name 'c' is not defined
Po użyciu polecenia „p (print)” w pdb, ten skrypt wypisuje tylko „aaa”. Po tym następuje błąd, ponieważ ustawiliśmy punkt przerwania na a = "aaa".
Podobnie możemy uruchomić skrypt, zmieniając punkty przerwania i zobaczyć różnicę w wynikach -
import pdb
a = "aaa"
b = "bbb"
c = "ccc"
pdb.set_trace()
final = a + b + c
print (final)
Wynik
--Return--
> <ipython-input-9-a59ef5caf723>(5)<module>()->None
-> pdb.set_trace()
(Pdb) p a
'aaa'
(Pdb) p b
'bbb'
(Pdb) p c
'ccc'
(Pdb) p final
*** NameError: name 'final' is not defined
(Pdb) exit
W poniższym skrypcie ustawiamy punkt przerwania w ostatniej linii programu -
import pdb
a = "aaa"
b = "bbb"
c = "ccc"
final = a + b + c
pdb.set_trace()
print (final)
Dane wyjściowe są następujące -
--Return--
> <ipython-input-11-8019b029997d>(6)<module>()->None
-> pdb.set_trace()
(Pdb) p a
'aaa'
(Pdb) p b
'bbb'
(Pdb) p c
'ccc'
(Pdb) p final
'aaabbbccc'
(Pdb)
W tym rozdziale dowiemy się, jak benchmarking i profilowanie pomagają w rozwiązywaniu problemów z wydajnością.
Załóżmy, że napisaliśmy kod, który również daje pożądany rezultat, ale co zrobić, jeśli chcemy uruchomić ten kod nieco szybciej, ponieważ potrzeby się zmieniły. W takim przypadku musimy dowiedzieć się, które części naszego kodu spowalniają cały program. W takim przypadku przydatne mogą być testy porównawcze i profilowanie.
Co to jest analiza porównawcza?
Benchmarking ma na celu ocenę czegoś przez porównanie ze standardem. Jednak pojawia się tutaj pytanie, czym byłby benchmarking i dlaczego potrzebujemy go w przypadku programowania oprogramowania. Benchmarking kodu oznacza, jak szybko kod jest wykonywany i gdzie znajduje się wąskie gardło. Jednym z głównych powodów testów porównawczych jest optymalizacja kodu.
Jak działa benchmarking?
Jeśli mówimy o działaniu benchmarkingu, musimy zacząć od analizy porównawczej całego programu jako jednego aktualnego stanu, a następnie możemy połączyć mikro testy porównawcze, a następnie rozłożyć program na mniejsze programy. Aby znaleźć wąskie gardła w naszym programie i zoptymalizować go. Innymi słowy, możemy to rozumieć jako rozbicie dużego i trudnego problemu na serię mniejszych i nieco łatwiejszych problemów w celu ich optymalizacji.
Moduł Pythona do testów porównawczych
W Pythonie mamy domyślnie moduł do testów porównawczych o nazwie timeit. Z pomocątimeit modułu, możemy zmierzyć wydajność małego fragmentu kodu Pythona w naszym głównym programie.
Przykład
W poniższym skrypcie Python importujemy plik timeit moduł, który dodatkowo mierzy czas potrzebny do wykonania dwóch funkcji - functionA i functionB -
import timeit
import time
def functionA():
print("Function A starts the execution:")
print("Function A completes the execution:")
def functionB():
print("Function B starts the execution")
print("Function B completes the execution")
start_time = timeit.default_timer()
functionA()
print(timeit.default_timer() - start_time)
start_time = timeit.default_timer()
functionB()
print(timeit.default_timer() - start_time)
Po uruchomieniu powyższego skryptu otrzymamy czas wykonania obu funkcji, jak pokazano poniżej.
Wynik
Function A starts the execution:
Function A completes the execution:
0.0014599495514175942
Function B starts the execution
Function B completes the execution
0.0017024724827479076
Pisanie własnego timera za pomocą funkcji dekoratora
W Pythonie możemy stworzyć własny licznik czasu, który będzie działał tak samo jak timeitmoduł. Można to zrobić za pomocądecoratorfunkcjonować. Poniżej znajduje się przykład niestandardowego timera -
import random
import time
def timer_func(func):
def function_timer(*args, **kwargs):
start = time.time()
value = func(*args, **kwargs)
end = time.time()
runtime = end - start
msg = "{func} took {time} seconds to complete its execution."
print(msg.format(func = func.__name__,time = runtime))
return value
return function_timer
@timer_func
def Myfunction():
for x in range(5):
sleep_time = random.choice(range(1,3))
time.sleep(sleep_time)
if __name__ == '__main__':
Myfunction()
Powyższy skrypt w Pythonie pomaga w importowaniu modułów czasu losowego. Stworzyliśmy funkcję dekoratora timer_func (). Wewnątrz znajduje się funkcja function_timer (). Teraz funkcja zagnieżdżona pobierze czas przed wywołaniem przekazanej funkcji. Następnie czeka na powrót funkcji i przechwytuje czas zakończenia. W ten sposób możemy wreszcie sprawić, by skrypt Pythona wypisał czas wykonania. Skrypt wygeneruje dane wyjściowe, jak pokazano poniżej.
Wynik
Myfunction took 8.000457763671875 seconds to complete its execution.
Co to jest profilowanie?
Czasami programista chce zmierzyć pewne atrybuty, takie jak użycie pamięci, złożoność czasowa lub użycie określonych instrukcji dotyczących programów, aby zmierzyć rzeczywiste możliwości tego programu. Takie mierzenie programu nazywa się profilowaniem. Profilowanie wykorzystuje dynamiczną analizę programu do takich pomiarów.
W kolejnych sekcjach dowiemy się o różnych modułach Pythona do profilowania.
cProfile - wbudowany moduł
cProfilejest wbudowanym modułem Pythona do profilowania. Moduł jest rozszerzeniem C z rozsądnym narzutem, co czyni go odpowiednim do profilowania długo działających programów. Po uruchomieniu rejestruje wszystkie funkcje i czasy wykonania. Jest bardzo potężny, ale czasami trochę trudny do zinterpretowania i wykonania. W poniższym przykładzie używamy cProfile w poniższym kodzie -
Przykład
def increment_global():
global x
x += 1
def taskofThread(lock):
for _ in range(50000):
lock.acquire()
increment_global()
lock.release()
def main():
global x
x = 0
lock = threading.Lock()
t1 = threading.Thread(target=taskofThread, args=(lock,))
t2 = threading.Thread(target= taskofThread, args=(lock,))
t1.start()
t2.start()
t1.join()
t2.join()
if __name__ == "__main__":
for i in range(5):
main()
print("x = {1} after Iteration {0}".format(i,x))
Powyższy kod jest zapisany w pliku thread_increment.pyplik. Teraz wykonaj kod z cProfile w wierszu poleceń w następujący sposób -
(base) D:\ProgramData>python -m cProfile thread_increment.py
x = 100000 after Iteration 0
x = 100000 after Iteration 1
x = 100000 after Iteration 2
x = 100000 after Iteration 3
x = 100000 after Iteration 4
3577 function calls (3522 primitive calls) in 1.688 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
5 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap>:103(release)
5 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap>:143(__init__)
5 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap>:147(__enter__)
… … … …
Z powyższego wyniku jasno wynika, że cProfile wypisuje wszystkie 3577 wywołane funkcje, wraz z czasem spędzonym w każdej z nich oraz liczbą wywołań. Poniżej znajdują się kolumny, które otrzymaliśmy w wynikach -
ncalls - To liczba wykonanych połączeń.
tottime - Jest to całkowity czas spędzony w danej funkcji.
percall - Odnosi się do ilorazu czasu całkowitego podzielonego przez liczbę połączeń telefonicznych.
cumtime- Jest to łączny czas spędzony w tej i wszystkich podfunkcjach. Jest to nawet dokładne dla funkcji rekurencyjnych.
percall - Jest to iloraz czasu podzielonego przez prymitywne wywołania.
filename:lineno(function) - Zasadniczo zapewnia odpowiednie dane każdej funkcji.
Załóżmy, że musieliśmy utworzyć dużą liczbę wątków dla naszych zadań wielowątkowych. Byłoby to najbardziej kosztowne obliczeniowo, ponieważ może wystąpić wiele problemów z wydajnością z powodu zbyt wielu wątków. Głównym problemem może być ograniczenie przepustowości. Możemy rozwiązać ten problem, tworząc pulę wątków. Pula wątków może być zdefiniowana jako grupa wstępnie utworzonych i bezczynnych wątków, które są gotowe do wykonania pracy. Tworzenie puli wątków jest preferowane zamiast tworzenia wystąpienia nowych wątków dla każdego zadania, gdy musimy wykonać dużą liczbę zadań. Pula wątków może zarządzać współbieżnym wykonywaniem dużej liczby wątków w następujący sposób -
Jeśli wątek w puli wątków zakończy wykonywanie, można go ponownie wykorzystać.
Jeśli wątek zostanie zakończony, zostanie utworzony inny wątek, który zastąpi ten wątek.
Moduł Pythona - Concurrent.futures
Biblioteka standardowa Pythona zawiera rozszerzenie concurrent.futuresmoduł. Ten moduł został dodany w Pythonie 3.2, aby zapewnić programistom interfejs wysokiego poziomu do uruchamiania zadań asynchronicznych. Jest to warstwa abstrakcji znajdująca się na wierzchu modułów obsługi wątków i wieloprocesorowych Pythona, zapewniająca interfejs do uruchamiania zadań przy użyciu puli wątków lub procesów.
W kolejnych sekcjach dowiemy się o różnych klasach modułu concurrent.futures.
Klasa egzekutora
Executorjest abstrakcyjną klasą klasy concurrent.futuresModuł Pythona. Nie można go używać bezpośrednio i musimy użyć jednej z następujących konkretnych podklas -
- ThreadPoolExecutor
- ProcessPoolExecutor
ThreadPoolExecutor - podklasa betonu
Jest to jedna z konkretnych podklas klasy Executor. Podklasa korzysta z wielowątkowości i otrzymujemy pulę wątków do zgłaszania zadań. Ta pula przypisuje zadania do dostępnych wątków i planuje ich wykonanie.
Jak stworzyć ThreadPoolExecutor?
Z pomocą concurrent.futures moduł i jego konkretną podklasę Executor, możemy łatwo stworzyć pulę wątków. W tym celu musimy skonstruować plikThreadPoolExecutorz liczbą wątków, które chcemy w puli. Domyślnie jest to liczba 5. Następnie możemy przesłać zadanie do puli wątków. Kiedy mysubmit() zadanie, otrzymujemy z powrotem Future. Obiekt Future ma metodę o nazwiedone(), który mówi, czy przyszłość się rozwiązała. W ten sposób została ustawiona wartość dla tego konkretnego przyszłego obiektu. Po zakończeniu zadania moduł wykonawczy puli wątków ustawia wartość na przyszły obiekt.
Przykład
from concurrent.futures import ThreadPoolExecutor
from time import sleep
def task(message):
sleep(2)
return message
def main():
executor = ThreadPoolExecutor(5)
future = executor.submit(task, ("Completed"))
print(future.done())
sleep(2)
print(future.done())
print(future.result())
if __name__ == '__main__':
main()
Wynik
False
True
Completed
W powyższym przykładzie a ThreadPoolExecutorzostał zbudowany z 5 wątków. Następnie do modułu wykonawczego puli wątków przesyłane jest zadanie, które będzie czekało 2 sekundy przed przekazaniem komunikatu. Jak widać z danych wyjściowych, zadanie nie kończy się przed upływem 2 sekund, więc pierwsze wywołaniedone()zwróci wartość False. Po 2 sekundach zadanie jest wykonane, a wynik na przyszłość uzyskujemy dzwoniąc doresult() metoda na nim.
Tworzenie wystąpienia ThreadPoolExecutor - Menedżer kontekstu
Inny sposób na utworzenie wystąpienia ThreadPoolExecutorodbywa się z pomocą menedżera kontekstu. Działa podobnie do metody zastosowanej w powyższym przykładzie. Główną zaletą używania menedżera kontekstu jest to, że wygląda dobrze składniowo. Tworzenie instancji można wykonać za pomocą następującego kodu -
with ThreadPoolExecutor(max_workers = 5) as executor
Przykład
Poniższy przykład został zapożyczony z dokumentacji Pythona. W tym przykładzie przede wszystkimconcurrent.futuresmoduł musi zostać zaimportowany. Następnie funkcja o nazwieload_url()zostanie utworzony, który załaduje żądany adres URL. Następnie funkcja tworzyThreadPoolExecutorz 5 wątkami w basenie. PlikThreadPoolExecutorzostał wykorzystany jako menedżer kontekstu. Możemy uzyskać wynik w przyszłości, dzwoniąc doresult() metoda na nim.
import concurrent.futures
import urllib.request
URLS = ['http://www.foxnews.com/',
'http://www.cnn.com/',
'http://europe.wsj.com/',
'http://www.bbc.co.uk/',
'http://some-made-up-domain.com/']
def load_url(url, timeout):
with urllib.request.urlopen(url, timeout = timeout) as conn:
return conn.read()
with concurrent.futures.ThreadPoolExecutor(max_workers = 5) as executor:
future_to_url = {executor.submit(load_url, url, 60): url for url in URLS}
for future in concurrent.futures.as_completed(future_to_url):
url = future_to_url[future]
try:
data = future.result()
except Exception as exc:
print('%r generated an exception: %s' % (url, exc))
else:
print('%r page is %d bytes' % (url, len(data)))
Wynik
Następujące byłoby wyjście powyższego skryptu Pythona -
'http://some-made-up-domain.com/' generated an exception: <urlopen error [Errno 11004] getaddrinfo failed>
'http://www.foxnews.com/' page is 229313 bytes
'http://www.cnn.com/' page is 168933 bytes
'http://www.bbc.co.uk/' page is 283893 bytes
'http://europe.wsj.com/' page is 938109 bytes
Użycie funkcji Executor.map ()
Python map()funkcja jest szeroko stosowana w wielu zadaniach. Jednym z takich zadań jest zastosowanie określonej funkcji do każdego elementu w elementach iterable. Podobnie, możemy odwzorować wszystkie elementy iteratora na funkcję i przesłać je jako niezależne zadaniaThreadPoolExecutor. Rozważ poniższy przykład skryptu w języku Python, aby zrozumieć, jak działa ta funkcja.
Przykład
W poniższym przykładzie funkcja mapy jest używana do zastosowania rozszerzenia square() funkcji do każdej wartości w tablicy wartości.
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import as_completed
values = [2,3,4,5]
def square(n):
return n * n
def main():
with ThreadPoolExecutor(max_workers = 3) as executor:
results = executor.map(square, values)
for result in results:
print(result)
if __name__ == '__main__':
main()
Wynik
Powyższy skrypt w Pythonie generuje następujące dane wyjściowe -
4
9
16
25
Pula procesów może być tworzona i używana w ten sam sposób, w jaki stworzyliśmy i wykorzystaliśmy pulę wątków. Pula procesów może być zdefiniowana jako grupa procesów wstępnie zainicjowanych i bezczynnych, które są gotowe do wykonania pracy. Tworzenie puli procesów jest preferowane w stosunku do tworzenia instancji nowych procesów dla każdego zadania, gdy potrzebujemy wykonać dużą liczbę zadań.
Moduł Pythona - Concurrent.futures
Biblioteka standardowa Pythona zawiera moduł o nazwie concurrent.futures. Ten moduł został dodany w Pythonie 3.2, aby zapewnić programistom interfejs wysokiego poziomu do uruchamiania zadań asynchronicznych. Jest to warstwa abstrakcji znajdująca się na wierzchu modułów obsługi wątków i wieloprocesorowych Pythona, zapewniająca interfejs do uruchamiania zadań przy użyciu puli wątków lub procesów.
W kolejnych sekcjach przyjrzymy się różnym podklasom modułu concurrent.futures.
Klasa egzekutora
Executor jest abstrakcyjną klasą klasy concurrent.futuresModuł Pythona. Nie można go używać bezpośrednio i musimy użyć jednej z następujących konkretnych podklas -
- ThreadPoolExecutor
- ProcessPoolExecutor
ProcessPoolExecutor - konkretna podklasa
Jest to jedna z konkretnych podklas klasy Executor. Wykorzystuje wieloprocesorowość i otrzymujemy pulę procesów do składania zadań. Ta pula przypisuje zadania do dostępnych procesów i planuje ich uruchomienie.
Jak stworzyć ProcessPoolExecutor?
Z pomocą concurrent.futures moduł i jego konkretną podklasę Executor, możemy łatwo stworzyć pulę procesów. W tym celu musimy skonstruować plikProcessPoolExecutorz liczbą procesów, które chcemy w puli. Domyślnie jest to liczba 5. Następnie następuje przesłanie zadania do puli procesów.
Przykład
Rozważymy teraz ten sam przykład, którego użyliśmy podczas tworzenia puli wątków, jedyną różnicą jest to, że teraz będziemy używać ProcessPoolExecutor zamiast ThreadPoolExecutor .
from concurrent.futures import ProcessPoolExecutor
from time import sleep
def task(message):
sleep(2)
return message
def main():
executor = ProcessPoolExecutor(5)
future = executor.submit(task, ("Completed"))
print(future.done())
sleep(2)
print(future.done())
print(future.result())
if __name__ == '__main__':
main()
Wynik
False
False
Completed
W powyższym przykładzie ProcessPoolExecutorzostał zbudowany z 5 wątków. Następnie do modułu wykonawczego puli procesów przesyłane jest zadanie, które będzie czekało 2 sekundy przed przekazaniem komunikatu. Jak widać z danych wyjściowych, zadanie nie kończy się przed upływem 2 sekund, więc pierwsze wywołaniedone()zwróci wartość False. Po 2 sekundach zadanie jest wykonane, a wynik na przyszłość uzyskujemy dzwoniąc doresult() metoda na nim.
Tworzenie wystąpienia ProcessPoolExecutor - Menedżer kontekstu
Innym sposobem na utworzenie instancji ProcessPoolExecutor jest użycie menedżera kontekstu. Działa podobnie do metody zastosowanej w powyższym przykładzie. Główną zaletą używania menedżera kontekstu jest to, że wygląda dobrze składniowo. Tworzenie instancji można wykonać za pomocą następującego kodu -
with ProcessPoolExecutor(max_workers = 5) as executor
Przykład
Aby lepiej zrozumieć, bierzemy ten sam przykład, który był używany podczas tworzenia puli wątków. W tym przykładzie musimy zacząć od zaimportowania plikuconcurrent.futuresmoduł. Następnie funkcja o nazwieload_url()zostanie utworzony, który załaduje żądany adres URL. PlikProcessPoolExecutorjest następnie tworzony z 5 liczbą wątków w puli. ProcesPoolExecutorzostał wykorzystany jako menedżer kontekstu. Możemy uzyskać wynik w przyszłości, dzwoniąc doresult() metoda na nim.
import concurrent.futures
from concurrent.futures import ProcessPoolExecutor
import urllib.request
URLS = ['http://www.foxnews.com/',
'http://www.cnn.com/',
'http://europe.wsj.com/',
'http://www.bbc.co.uk/',
'http://some-made-up-domain.com/']
def load_url(url, timeout):
with urllib.request.urlopen(url, timeout = timeout) as conn:
return conn.read()
def main():
with concurrent.futures.ProcessPoolExecutor(max_workers=5) as executor:
future_to_url = {executor.submit(load_url, url, 60): url for url in URLS}
for future in concurrent.futures.as_completed(future_to_url):
url = future_to_url[future]
try:
data = future.result()
except Exception as exc:
print('%r generated an exception: %s' % (url, exc))
else:
print('%r page is %d bytes' % (url, len(data)))
if __name__ == '__main__':
main()
Wynik
Powyższy skrypt w języku Python wygeneruje następujące dane wyjściowe -
'http://some-made-up-domain.com/' generated an exception: <urlopen error [Errno 11004] getaddrinfo failed>
'http://www.foxnews.com/' page is 229476 bytes
'http://www.cnn.com/' page is 165323 bytes
'http://www.bbc.co.uk/' page is 284981 bytes
'http://europe.wsj.com/' page is 967575 bytes
Użycie funkcji Executor.map ()
Python map()funkcja jest szeroko stosowana do wykonywania wielu zadań. Jednym z takich zadań jest zastosowanie określonej funkcji do każdego elementu w elementach iterable. Podobnie możemy zmapować wszystkie elementy iteratora na funkcję i przesłać je jako niezależne zadania doProcessPoolExecutor. Aby to zrozumieć, rozważ następujący przykład skryptu w języku Python.
Przykład
Rozważymy ten sam przykład, którego użyliśmy podczas tworzenia puli wątków przy użyciu Executor.map()funkcjonować. W przykładzie podanym poniżej, do zastosowania służy funkcja mapsquare() funkcji do każdej wartości w tablicy wartości.
from concurrent.futures import ProcessPoolExecutor
from concurrent.futures import as_completed
values = [2,3,4,5]
def square(n):
return n * n
def main():
with ProcessPoolExecutor(max_workers = 3) as executor:
results = executor.map(square, values)
for result in results:
print(result)
if __name__ == '__main__':
main()
Wynik
Powyższy skrypt w języku Python wygeneruje następujące dane wyjściowe
4
9
16
25
Kiedy używać ProcessPoolExecutor i ThreadPoolExecutor?
Teraz, gdy przestudiowaliśmy obie klasy Executorów - ThreadPoolExecutor i ProcessPoolExecutor, musimy wiedzieć, kiedy użyć którego executora. Musimy wybrać ProcessPoolExecutor w przypadku obciążeń związanych z procesorem i ThreadPoolExecutor w przypadku obciążeń związanych z we / wy.
Jeśli używamy ProcessPoolExecutor, wtedy nie musimy martwić się o GIL, ponieważ korzysta on z przetwarzania wieloprocesowego. Co więcej, czas wykonania będzie krótszy w porównaniu zThreadPoolExecution. Aby to zrozumieć, rozważ następujący przykład skryptu w języku Python.
Przykład
import time
import concurrent.futures
value = [8000000, 7000000]
def counting(n):
start = time.time()
while n > 0:
n -= 1
return time.time() - start
def main():
start = time.time()
with concurrent.futures.ProcessPoolExecutor() as executor:
for number, time_taken in zip(value, executor.map(counting, value)):
print('Start: {} Time taken: {}'.format(number, time_taken))
print('Total time taken: {}'.format(time.time() - start))
if __name__ == '__main__':
main()
Wynik
Start: 8000000 Time taken: 1.5509998798370361
Start: 7000000 Time taken: 1.3259999752044678
Total time taken: 2.0840001106262207
Example- Python script with ThreadPoolExecutor:
import time
import concurrent.futures
value = [8000000, 7000000]
def counting(n):
start = time.time()
while n > 0:
n -= 1
return time.time() - start
def main():
start = time.time()
with concurrent.futures.ThreadPoolExecutor() as executor:
for number, time_taken in zip(value, executor.map(counting, value)):
print('Start: {} Time taken: {}'.format(number, time_taken))
print('Total time taken: {}'.format(time.time() - start))
if __name__ == '__main__':
main()
Wynik
Start: 8000000 Time taken: 3.8420000076293945
Start: 7000000 Time taken: 3.6010000705718994
Total time taken: 3.8480000495910645
Na podstawie wyników obu powyższych programów możemy zobaczyć różnicę w czasie wykonywania podczas używania ProcessPoolExecutor i ThreadPoolExecutor.
W tym rozdziale skupimy się bardziej na porównaniu między przetwarzaniem wieloprocesowym a wielowątkowością.
Wieloprocesowość
Jest to użycie dwóch lub więcej jednostek CPU w jednym systemie komputerowym. Jest to najlepsze podejście do pełnego wykorzystania potencjału naszego sprzętu poprzez wykorzystanie pełnej liczby rdzeni procesora dostępnych w naszym systemie komputerowym.
Wielowątkowość
Jest to zdolność procesora do zarządzania wykorzystaniem systemu operacyjnego poprzez jednoczesne wykonywanie wielu wątków. Główną ideą wielowątkowości jest osiągnięcie równoległości poprzez podzielenie procesu na wiele wątków.
Poniższa tabela przedstawia niektóre z ważnych różnic między nimi -
Wieloprocesowość | Wieloprogramowanie |
---|---|
Wieloprocesorowość oznacza przetwarzanie wielu procesów w tym samym czasie przez wiele procesorów. | Wieloprogramowanie zachowuje kilka programów w pamięci głównej w tym samym czasie i wykonuje je jednocześnie, wykorzystując jeden procesor. |
Wykorzystuje wiele procesorów. | Wykorzystuje pojedynczy procesor. |
Umożliwia równoległe przetwarzanie. | Następuje przełączanie kontekstów. |
Mniej czasu zajmuje przetwarzanie zadań. | Więcej czasu zajmuje przetwarzanie zadań. |
Ułatwia bardzo efektywne wykorzystanie urządzeń systemu komputerowego. | Mniej wydajne niż przetwarzanie wieloprocesowe. |
Zwykle droższe. | Takie systemy są tańsze. |
Eliminacja wpływu globalnej blokady tłumacza (GIL)
Podczas pracy z aplikacjami współbieżnymi w Pythonie występuje ograniczenie o nazwie GIL (Global Interpreter Lock). GIL nigdy nie pozwala nam na wykorzystanie wielu rdzeni procesora, dlatego możemy powiedzieć, że w Pythonie nie ma prawdziwych wątków. GIL to mutex - blokada wzajemnego wykluczania, która sprawia, że rzeczy są bezpieczne. Innymi słowy, możemy powiedzieć, że GIL zapobiega równoległemu wykonywaniu kodu Pythona przez wiele wątków. Blokada może być utrzymywana tylko przez jeden wątek naraz, a jeśli chcemy wykonać wątek, musi najpierw uzyskać blokadę.
Dzięki zastosowaniu wieloprocesorowości możemy skutecznie ominąć ograniczenie spowodowane przez GIL -
Korzystając z przetwarzania wieloprocesowego, wykorzystujemy możliwości wielu procesów, a zatem wykorzystujemy wiele instancji GIL.
Z tego powodu nie ma ograniczeń co do wykonywania kodu bajtowego jednego wątku w naszych programach w dowolnym momencie.
Uruchamianie procesów w Pythonie
Do uruchomienia procesu w Pythonie w module wieloprocesorowym można użyć następujących trzech metod -
- Fork
- Spawn
- Forkserver
Tworzenie procesu za pomocą Fork
Polecenie Fork to standardowe polecenie występujące w systemie UNIX. Służy do tworzenia nowych procesów zwanych procesami potomnymi. Ten proces potomny działa równolegle z procesem nazywanym procesem nadrzędnym. Te procesy potomne są również identyczne z ich procesami nadrzędnymi i dziedziczą wszystkie zasoby dostępne dla elementu nadrzędnego. Podczas tworzenia procesu za pomocą Fork używane są następujące wywołania systemowe -
fork()- Jest to wywołanie systemowe generalnie zaimplementowane w jądrze. Służy do tworzenia kopii procesu. P>
getpid() - To wywołanie systemowe zwraca identyfikator procesu (PID) procesu wywołującego.
Przykład
Poniższy przykład skryptu w Pythonie pomoże ci zrozumieć, jak utworzyć nowy proces potomny i uzyskać identyfikatory PID procesów potomnych i nadrzędnych -
import os
def child():
n = os.fork()
if n > 0:
print("PID of Parent process is : ", os.getpid())
else:
print("PID of Child process is : ", os.getpid())
child()
Wynik
PID of Parent process is : 25989
PID of Child process is : 25990
Tworzenie procesu za pomocą Spawn
Spawn oznacza rozpoczęcie czegoś nowego. Stąd tworzenie procesu oznacza utworzenie nowego procesu przez proces macierzysty. Proces nadrzędny kontynuuje wykonywanie asynchronicznie lub czeka, aż proces potomny zakończy wykonywanie. Wykonaj następujące kroki, aby utworzyć proces -
Importowanie modułu wieloprocesorowego.
Tworzenie procesu obiektowego.
Rozpoczęcie czynności procesu przez wywołanie start() metoda.
Czekam, aż proces zakończy swoją pracę i zakończ, dzwoniąc join() metoda.
Przykład
Poniższy przykład skryptu w Pythonie pomaga w uruchomieniu trzech procesów
import multiprocessing
def spawn_process(i):
print ('This is process: %s' %i)
return
if __name__ == '__main__':
Process_jobs = []
for i in range(3):
p = multiprocessing.Process(target = spawn_process, args = (i,))
Process_jobs.append(p)
p.start()
p.join()
Wynik
This is process: 0
This is process: 1
This is process: 2
Tworzenie procesu za pomocą Forkserver
Mechanizm Forkserver jest dostępny tylko na wybranych platformach UNIX, które obsługują przekazywanie deskryptorów plików przez potoki Unix. Rozważ następujące punkty, aby zrozumieć działanie mechanizmu Forkserver -
Serwer jest tworzony przy użyciu mechanizmu Forkserver do uruchamiania nowego procesu.
Następnie serwer otrzymuje polecenie i obsługuje wszystkie żądania tworzenia nowych procesów.
Aby stworzyć nowy proces, nasz program w Pythonie wyśle żądanie do Forkserver i stworzy dla nas proces.
W końcu możemy wykorzystać ten nowo utworzony proces w naszych programach.
Procesy demona w Pythonie
Pyton multiprocessingmoduł pozwala nam mieć procesy demonów poprzez jego opcję demoniczną. Procesy demona lub procesy działające w tle działają podobnie jak wątki demona. Aby wykonać proces w tle, musimy ustawić flagę demona na true. Proces demona będzie działał tak długo, jak długo będzie wykonywany proces główny i zakończy się po zakończeniu jego wykonywania lub gdy główny program zostanie zabity.
Przykład
Tutaj używamy tego samego przykładu, który jest używany w wątkach demonów. Jedyną różnicą jest zmiana modułu zmultithreading do multiprocessingi ustawiając flagę demona na true. Jednak nastąpiłaby zmiana w produkcji, jak pokazano poniżej -
import multiprocessing
import time
def nondaemonProcess():
print("starting my Process")
time.sleep(8)
print("ending my Process")
def daemonProcess():
while True:
print("Hello")
time.sleep(2)
if __name__ == '__main__':
nondaemonProcess = multiprocessing.Process(target = nondaemonProcess)
daemonProcess = multiprocessing.Process(target = daemonProcess)
daemonProcess.daemon = True
nondaemonProcess.daemon = False
daemonProcess.start()
nondaemonProcess.start()
Wynik
starting my Process
ending my Process
Dane wyjściowe są inne niż te generowane przez wątki demona, ponieważ proces bez trybu demona ma dane wyjściowe. W związku z tym proces demoniczny kończy się automatycznie po zakończeniu działania głównych programów, aby uniknąć trwałości uruchomionych procesów.
Zakończenie procesów w Pythonie
Możemy natychmiast zabić lub zakończyć proces przy użyciu rozszerzenia terminate()metoda. Użyjemy tej metody do zakończenia procesu potomnego, który został utworzony za pomocą funkcji, bezpośrednio przed zakończeniem jego wykonania.
Przykład
import multiprocessing
import time
def Child_process():
print ('Starting function')
time.sleep(5)
print ('Finished function')
P = multiprocessing.Process(target = Child_process)
P.start()
print("My Process has terminated, terminating main thread")
print("Terminating Child Process")
P.terminate()
print("Child Process successfully terminated")
Wynik
My Process has terminated, terminating main thread
Terminating Child Process
Child Process successfully terminated
Wynik pokazuje, że program kończy pracę przed wykonaniem procesu potomnego, który został utworzony za pomocą funkcji Child_process (). Oznacza to, że proces potomny został pomyślnie zakończony.
Identyfikowanie bieżącego procesu w Pythonie
Każdy proces w systemie operacyjnym ma tożsamość znaną jako PID. W Pythonie możemy znaleźć PID bieżącego procesu za pomocą następującego polecenia -
import multiprocessing
print(multiprocessing.current_process().pid)
Przykład
Poniższy przykład skryptu w Pythonie pomaga znaleźć PID procesu głównego, a także PID procesu potomnego -
import multiprocessing
import time
def Child_process():
print("PID of Child Process is: {}".format(multiprocessing.current_process().pid))
print("PID of Main process is: {}".format(multiprocessing.current_process().pid))
P = multiprocessing.Process(target=Child_process)
P.start()
P.join()
Wynik
PID of Main process is: 9401
PID of Child Process is: 9402
Korzystanie z procesu w podklasie
Możemy tworzyć wątki, dzieląc rozszerzenie threading.Threadklasa. Ponadto możemy również tworzyć procesy, klasyfikując plikimultiprocessing.Processklasa. Aby użyć procesu w podklasie, musimy wziąć pod uwagę następujące punkty -
Musimy zdefiniować nową podklasę klasy Process klasa.
Musimy zastąpić _init_(self [,args] ) klasa.
Musimy zastąpić run(self [,args] ) metoda wdrożenia czego Process
Musimy rozpocząć proces od wywołaniastart() metoda.
Przykład
import multiprocessing
class MyProcess(multiprocessing.Process):
def run(self):
print ('called run method in process: %s' %self.name)
return
if __name__ == '__main__':
jobs = []
for i in range(5):
P = MyProcess()
jobs.append(P)
P.start()
P.join()
Wynik
called run method in process: MyProcess-1
called run method in process: MyProcess-2
called run method in process: MyProcess-3
called run method in process: MyProcess-4
called run method in process: MyProcess-5
Moduł wieloprocesorowy Pythona - klasa puli
Jeśli mówimy o prostej równoległości processingzadań w naszych aplikacjach Python, a następnie moduł wieloprocesorowy dostarcza nam klasę Pool. Następujące metodyPool class może służyć do uruchamiania wielu procesów potomnych w naszym głównym programie
apply (), metoda
Ta metoda jest podobna do.submit()metoda .ThreadPoolExecutor.Blokuje się, aż wynik będzie gotowy.
Apply_async (), metoda
Kiedy potrzebujemy równoległego wykonywania naszych zadań, musimy użyćapply_async()metoda przesyłania zadań do puli. Jest to operacja asynchroniczna, która nie zablokuje głównego wątku, dopóki nie zostaną wykonane wszystkie procesy potomne.
map () metoda
Podobnie jak apply()metoda, blokuje również, dopóki wynik nie będzie gotowy. Jest odpowiednikiem wbudowanegomap() funkcja, która dzieli iterowalne dane na kilka porcji i przesyła do puli procesów jako oddzielne zadania.
map_async (), metoda
Jest to wariant map() metoda jako apply_async() jest do apply()metoda. Zwraca obiekt wynikowy. Kiedy wynik stanie się gotowy, zostanie do niego zastosowane wywołanie. Wywołanie musi zostać zakończone natychmiast; w przeciwnym razie wątek obsługujący wyniki zostanie zablokowany.
Przykład
Poniższy przykład pomoże Ci zaimplementować pulę procesów do wykonywania równoległego wykonywania. Proste obliczenie kwadratu liczby zostało przeprowadzone przez zastosowaniesquare() funkcji za pośrednictwem multiprocessing.Poolmetoda. Następniepool.map() został użyty do przesłania 5, ponieważ dane wejściowe to lista liczb całkowitych od 0 do 4. Wynik zostanie zapisany w p_outputs i jest drukowany.
def square(n):
result = n*n
return result
if __name__ == '__main__':
inputs = list(range(5))
p = multiprocessing.Pool(processes = 4)
p_outputs = pool.map(function_square, inputs)
p.close()
p.join()
print ('Pool :', p_outputs)
Wynik
Pool : [0, 1, 4, 9, 16]
Komunikacja między procesami oznacza wymianę danych między procesami. Konieczna jest wymiana danych pomiędzy procesami w celu stworzenia równoległej aplikacji. Poniższy diagram przedstawia różne mechanizmy komunikacji do synchronizacji między wieloma procesami podrzędnymi
Różne mechanizmy komunikacji
W tej sekcji poznamy różne mechanizmy komunikacji. Mechanizmy opisano poniżej -
Kolejki
Kolejki mogą być używane w programach wieloprocesowych. Klasa Queuemultiprocessing moduł jest podobny do Queue.Queueklasa. W związku z tym można użyć tego samego interfejsu API.Multiprocessing.Queue zapewnia nam bezpieczny dla wątków i procesów mechanizm komunikacji między procesami FIFO (first-in-first-out).
Przykład
Poniżej znajduje się prosty przykład zaczerpnięty z oficjalnej dokumentacji Pythona na temat przetwarzania wieloprocesowego, aby zrozumieć koncepcję klasy Queue przetwarzania wieloprocesowego.
from multiprocessing import Process, Queue
import queue
import random
def f(q):
q.put([42, None, 'hello'])
def main():
q = Queue()
p = Process(target = f, args = (q,))
p.start()
print (q.get())
if __name__ == '__main__':
main()
Wynik
[42, None, 'hello']
Rury
Jest to struktura danych, która służy do komunikacji między procesami w programach wieloprocesowych. Funkcja Pipe () zwraca parę obiektów połączeniowych połączonych potokiem, który domyślnie jest dupleksowy (dwukierunkowy). Działa w następujący sposób -
Zwraca parę obiektów połączeniowych, które reprezentują dwa końce rury.
Każdy obiekt ma dwie metody - send() i recv()do komunikacji między procesami.
Przykład
Poniżej znajduje się prosty przykład zaczerpnięty z oficjalnych dokumentów Pythona na temat przetwarzania wieloprocesowego, aby zrozumieć pojęcie Pipe() funkcja wieloprocesowości.
from multiprocessing import Process, Pipe
def f(conn):
conn.send([42, None, 'hello'])
conn.close()
if __name__ == '__main__':
parent_conn, child_conn = Pipe()
p = Process(target = f, args = (child_conn,))
p.start()
print (parent_conn.recv())
p.join()
Wynik
[42, None, 'hello']
Menedżer
Manager to klasa modułu wieloprocesorowego, który zapewnia sposób koordynowania wymiany informacji między wszystkimi jego użytkownikami. Obiekt menedżera steruje procesem serwera, który zarządza obiektami współużytkowanymi i umożliwia innym procesom manipulowanie nimi. Innymi słowy, menedżerowie zapewniają sposób tworzenia danych, które można udostępniać między różnymi procesami. Poniżej przedstawiono różne właściwości obiektu menedżera -
Główną właściwością menedżera jest sterowanie procesem serwera, który zarządza współdzielonymi obiektami.
Inną ważną właściwością jest aktualizowanie wszystkich współdzielonych obiektów, gdy modyfikuje je dowolny proces.
Przykład
Poniżej znajduje się przykład wykorzystujący obiekt menedżera do tworzenia rekordu listy w procesie serwera, a następnie dodawania nowego rekordu do tej listy.
import multiprocessing
def print_records(records):
for record in records:
print("Name: {0}\nScore: {1}\n".format(record[0], record[1]))
def insert_record(record, records):
records.append(record)
print("A New record is added\n")
if __name__ == '__main__':
with multiprocessing.Manager() as manager:
records = manager.list([('Computers', 1), ('Histoty', 5), ('Hindi',9)])
new_record = ('English', 3)
p1 = multiprocessing.Process(target = insert_record, args = (new_record, records))
p2 = multiprocessing.Process(target = print_records, args = (records,))
p1.start()
p1.join()
p2.start()
p2.join()
Wynik
A New record is added
Name: Computers
Score: 1
Name: Histoty
Score: 5
Name: Hindi
Score: 9
Name: English
Score: 3
Pojęcie przestrzeni nazw w menedżerze
Manager Class zawiera koncepcję przestrzeni nazw, która jest szybką metodą udostępniania kilku atrybutów w wielu procesach. Przestrzenie nazw nie zawierają żadnej publicznej metody, którą można wywołać, ale mają atrybuty z możliwością zapisu.
Przykład
Poniższy przykład skryptu w języku Python pomaga nam wykorzystać przestrzenie nazw do udostępniania danych w procesie głównym i podrzędnym -
import multiprocessing
def Mng_NaSp(using_ns):
using_ns.x +=5
using_ns.y *= 10
if __name__ == '__main__':
manager = multiprocessing.Manager()
using_ns = manager.Namespace()
using_ns.x = 1
using_ns.y = 1
print ('before', using_ns)
p = multiprocessing.Process(target = Mng_NaSp, args = (using_ns,))
p.start()
p.join()
print ('after', using_ns)
Wynik
before Namespace(x = 1, y = 1)
after Namespace(x = 6, y = 10)
Tablica i wartość Ctypes
Moduł wieloprocesorowy zapewnia obiekty Array i Value do przechowywania danych w mapie pamięci współdzielonej. Array jest tablicą ctypes przydzieloną z pamięci współdzielonej i Value jest obiektem ctypes przydzielonym z pamięci współdzielonej.
Aby być z, zaimportuj Process, Value, Array z przetwarzania wieloprocesowego.
Przykład
Poniższy skrypt w języku Python jest przykładem zaczerpniętym z dokumentacji Pythona w celu wykorzystania tablicy i wartości Ctypes do udostępniania niektórych danych między procesami.
def f(n, a):
n.value = 3.1415927
for i in range(len(a)):
a[i] = -a[i]
if __name__ == '__main__':
num = Value('d', 0.0)
arr = Array('i', range(10))
p = Process(target = f, args = (num, arr))
p.start()
p.join()
print (num.value)
print (arr[:])
Wynik
3.1415927
[0, -1, -2, -3, -4, -5, -6, -7, -8, -9]
Komunikowanie procesów sekwencyjnych (CSP)
CSP służy do zilustrowania interakcji systemów z innymi systemami wyposażonymi w modele współbieżne. CSP to platforma do pisania współbieżnego lub programu poprzez przekazywanie komunikatów, dlatego jest skuteczna do opisywania współbieżności.
Biblioteka Pythona - PyCSP
Do implementacji podstawowych prymitywów znalezionych w CSP, Python ma bibliotekę o nazwie PyCSP. Dzięki temu implementacja jest bardzo krótka i czytelna, dzięki czemu można ją bardzo łatwo zrozumieć. Poniżej przedstawiono podstawową sieć procesową PyCSP -
W powyższej sieci procesów PyCSP istnieją dwa procesy - Proces1 i Proces 2. Procesy te komunikują się, przesyłając komunikaty przez dwa kanały - kanał 1 i kanał 2.
Instalowanie PyCSP
Za pomocą następującego polecenia możemy zainstalować bibliotekę Python PyCSP -
pip install PyCSP
Przykład
Poniższy skrypt w Pythonie jest prostym przykładem uruchamiania dwóch procesów równolegle względem siebie. Odbywa się to za pomocą biblioteki PyCSP Python -
from pycsp.parallel import *
import time
@process
def P1():
time.sleep(1)
print('P1 exiting')
@process
def P2():
time.sleep(1)
print('P2 exiting')
def main():
Parallel(P1(), P2())
print('Terminating')
if __name__ == '__main__':
main()
W powyższym skrypcie dwie funkcje, a mianowicie P1 i P2 zostały stworzone, a następnie ozdobione @process za przekształcenie ich w procesy.
Wynik
P2 exiting
P1 exiting
Terminating
Programowanie sterowane zdarzeniami koncentruje się na wydarzeniach. Ostatecznie przebieg programu zależy od wydarzeń. Do tej pory mieliśmy do czynienia z sekwencyjnym lub równoległym modelem wykonania, ale model mający koncepcję programowania sterowanego zdarzeniami nazywany jest modelem asynchronicznym. Programowanie sterowane zdarzeniami zależy od pętli zdarzeń, która zawsze nasłuchuje nowych nadchodzących zdarzeń. Działanie programowania sterowanego zdarzeniami zależy od zdarzeń. Po zapętleniu zdarzenia decydują o tym, co należy wykonać i w jakiej kolejności. Poniższy schemat blokowy pomoże Ci zrozumieć, jak to działa -
Moduł Pythona - Asyncio
Moduł Asyncio został dodany w Pythonie 3.4 i zapewnia infrastrukturę do pisania jednowątkowego kodu współbieżnego przy użyciu współprogramów. Poniżej przedstawiono różne koncepcje używane przez moduł Asyncio -
Pętla zdarzeń
Pętla zdarzeń to funkcja obsługująca wszystkie zdarzenia w kodzie obliczeniowym. Działa na okrągło podczas wykonywania całego programu i śledzi przychodzące i wykonywane zdarzenia. Moduł Asyncio umożliwia pojedynczą pętlę zdarzeń na proces. Poniżej znajduje się kilka metod dostarczanych przez moduł Asyncio do zarządzania pętlą zdarzeń -
loop = get_event_loop() - Ta metoda zapewni pętlę zdarzeń dla bieżącego kontekstu.
loop.call_later(time_delay,callback,argument) - Ta metoda organizuje wywołanie zwrotne, które ma zostać wywołane po podanych sekundach time_delay.
loop.call_soon(callback,argument)- Ta metoda organizuje wywołanie zwrotne, które ma zostać wywołane tak szybko, jak to możliwe. Wywołanie zwrotne jest wywoływane po powrocie funkcji call_soon () i po powrocie sterowania do pętli zdarzeń.
loop.time() - Ta metoda służy do zwracania aktualnego czasu zgodnie z wewnętrznym zegarem pętli zdarzeń.
asyncio.set_event_loop() - Ta metoda ustawi pętlę zdarzeń dla bieżącego kontekstu na pętlę.
asyncio.new_event_loop() - Ta metoda utworzy i zwróci nowy obiekt pętli zdarzeń.
loop.run_forever() - Ta metoda będzie działać do momentu wywołania metody stop ().
Przykład
Poniższy przykład pętli zdarzeń pomaga w drukowaniu hello worldużywając metody get_event_loop (). Ten przykład pochodzi z oficjalnej dokumentacji Pythona.
import asyncio
def hello_world(loop):
print('Hello World')
loop.stop()
loop = asyncio.get_event_loop()
loop.call_soon(hello_world, loop)
loop.run_forever()
loop.close()
Wynik
Hello World
Futures
Jest to zgodne z klasą concurrent.futures.Future, która reprezentuje obliczenia, które nie zostały wykonane. Istnieją następujące różnice między asyncio.futures.Future i concurrent.futures.Future -
metody result () iception () nie pobierają argumentu timeout i zgłaszają wyjątek, gdy przyszłość nie jest jeszcze wykonana.
Wywołania zwrotne zarejestrowane za pomocą add_done_callback () są zawsze wywoływane za pośrednictwem funkcji call_soon () pętli zdarzeń.
Klasa asyncio.futures.Future nie jest zgodna z funkcjami wait () i as_completed () w pakiecie concurrent.futures.
Przykład
Poniżej znajduje się przykład, który pomoże ci zrozumieć, jak używać klasy asyncio.futures.future.
import asyncio
async def Myoperation(future):
await asyncio.sleep(2)
future.set_result('Future Completed')
loop = asyncio.get_event_loop()
future = asyncio.Future()
asyncio.ensure_future(Myoperation(future))
try:
loop.run_until_complete(future)
print(future.result())
finally:
loop.close()
Wynik
Future Completed
Coroutines
Koncepcja coroutines w Asyncio jest podobna do koncepcji standardowego obiektu Thread w module Threading. To jest uogólnienie koncepcji podprogramu. Program może zostać zawieszony w trakcie wykonywania, aby czekał na przetwarzanie zewnętrzne i wrócił z miejsca, w którym został zatrzymany, gdy przetwarzanie zewnętrzne zostało zakończone. Poniższe dwa sposoby pomagają nam we wdrażaniu programów -
async def, funkcja ()
Jest to metoda implementacji coroutines w module Asyncio. Poniżej znajduje się skrypt w Pythonie dla tego samego -
import asyncio
async def Myoperation():
print("First Coroutine")
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(Myoperation())
finally:
loop.close()
Wynik
First Coroutine
@ asyncio.coroutine dekorator
Inną metodą implementacji coroutines jest wykorzystanie generatorów z dekoratorem @ asyncio.coroutine. Poniżej znajduje się skrypt w Pythonie dla tego samego -
import asyncio
@asyncio.coroutine
def Myoperation():
print("First Coroutine")
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(Myoperation())
finally:
loop.close()
Wynik
First Coroutine
Zadania
Ta podklasa modułu Asyncio jest odpowiedzialna za równoległe wykonywanie programów w pętli zdarzeń. Poniższy skrypt w Pythonie jest przykładem równoległego przetwarzania niektórych zadań.
import asyncio
import time
async def Task_ex(n):
time.sleep(1)
print("Processing {}".format(n))
async def Generator_task():
for i in range(10):
asyncio.ensure_future(Task_ex(i))
int("Tasks Completed")
asyncio.sleep(2)
loop = asyncio.get_event_loop()
loop.run_until_complete(Generator_task())
loop.close()
Wynik
Tasks Completed
Processing 0
Processing 1
Processing 2
Processing 3
Processing 4
Processing 5
Processing 6
Processing 7
Processing 8
Processing 9
Transporty
Moduł Asyncio udostępnia klasy transportowe do realizacji różnego rodzaju komunikacji. Te klasy nie są bezpieczne dla wątków i zawsze są sparowane z instancją protokołu po ustanowieniu kanału komunikacyjnego.
Poniżej przedstawiono różne typy transportów odziedziczone po BaseTransport -
ReadTransport - To jest interfejs dla transportów tylko do odczytu.
WriteTransport - To jest interfejs dla transportów tylko do zapisu.
DatagramTransport - To jest interfejs do przesyłania danych.
BaseSubprocessTransport - Podobny do klasy BaseTransport.
Poniżej znajduje się pięć różnych metod klasy BaseTransport, które są następnie przejściowe dla czterech typów transportu -
close() - To zamyka transport.
is_closing() - Ta metoda zwróci wartość true, jeśli transport się zamyka lub jest już zamknięty.
get_extra_info(name, default = none) - To da nam dodatkowe informacje o transporcie.
get_protocol() - Ta metoda zwróci bieżący protokół.
Protokoły
Moduł Asyncio udostępnia klasy bazowe, które można podklasować w celu zaimplementowania protokołów sieciowych. Klasy te są używane w połączeniu z transportami; protokół analizuje przychodzące dane i prosi o zapis danych wychodzących, podczas gdy transport jest odpowiedzialny za faktyczne I / O i buforowanie. Poniżej przedstawiono trzy klasy protokołu -
Protocol - Jest to klasa bazowa do implementowania protokołów przesyłania strumieniowego do użytku z transportami TCP i SSL.
DatagramProtocol - To jest klasa bazowa do implementowania protokołów datagramowych do użytku z transportami UDP.
SubprocessProtocol - Jest to klasa bazowa do implementowania protokołów komunikujących się z procesami potomnymi za pośrednictwem zestawu potoków jednokierunkowych.
Programowanie reaktywne to paradygmat programowania, który zajmuje się przepływem danych i propagacją zmian. Oznacza to, że gdy przepływ danych jest emitowany przez jeden komponent, zmiana będzie propagowana do innych komponentów przez reaktywną bibliotekę programowania. Propagacja zmiany będzie kontynuowana, aż dotrze do końcowego odbiorcy. Różnica między programowaniem sterowanym zdarzeniami a programowaniem reaktywnym polega na tym, że programowanie sterowane zdarzeniami obraca się wokół zdarzeń, a programowanie reaktywne obraca się wokół danych.
ReactiveX lub RX do programowania reaktywnego
ReactiveX lub Raective Extension to najbardziej znana implementacja programowania reaktywnego. Działanie ReactiveX zależy od następujących dwóch klas -
Obserwowalna klasa
Ta klasa jest źródłem strumienia danych lub zdarzeń i pakuje przychodzące dane, dzięki czemu dane mogą być przekazywane z jednego wątku do drugiego. Nie poda danych, dopóki jakiś obserwator go nie zapisze.
Klasa obserwatora
Ta klasa zużywa strumień danych wyemitowany przez observable. Może być wielu obserwatorów z możliwością obserwacji, a każdy obserwator otrzyma każdy wyemitowany element danych. Obserwator może otrzymać trzy rodzaje zdarzeń, subskrybując obserwowalne -
on_next() event - Oznacza to, że strumień danych zawiera element.
on_completed() event - Oznacza to koniec emisji i żadne elementy nie nadchodzą.
on_error() event - Oznacza również koniec emisji, ale w przypadku zgłoszenia błędu observable.
RxPY - moduł Pythona do programowania reaktywnego
RxPY to moduł Pythona, który może być używany do programowania reaktywnego. Musimy upewnić się, że moduł jest zainstalowany. Do zainstalowania modułu RxPY można użyć następującego polecenia -
pip install RxPY
Przykład
Poniżej znajduje się skrypt w Pythonie, który używa RxPY moduł i jego klasy Observable i Observe forprogramowanie reaktywne. Zasadniczo istnieją dwie klasy -
get_strings() - za pobranie strun od obserwatora.
PrintObserver()- do drukowania napisów od obserwatora. Wykorzystuje wszystkie trzy zdarzenia klasy obserwatorów. Używa również klasy subscribe ().
from rx import Observable, Observer
def get_strings(observer):
observer.on_next("Ram")
observer.on_next("Mohan")
observer.on_next("Shyam")
observer.on_completed()
class PrintObserver(Observer):
def on_next(self, value):
print("Received {0}".format(value))
def on_completed(self):
print("Finished")
def on_error(self, error):
print("Error: {0}".format(error))
source = Observable.create(get_strings)
source.subscribe(PrintObserver())
Wynik
Received Ram
Received Mohan
Received Shyam
Finished
Biblioteka PyFunctional do programowania reaktywnego
PyFunctionalto kolejna biblioteka Pythona, której można użyć do programowania reaktywnego. Umożliwia nam tworzenie programów funkcjonalnych przy użyciu języka programowania Python. Jest to przydatne, ponieważ pozwala nam tworzyć potoki danych przy użyciu połączonych operatorów funkcjonalnych.
Różnica między RxPY i PyFunctional
Obie biblioteki są używane do programowania reaktywnego i obsługują strumień w podobny sposób, ale główna różnica między nimi zależy od obsługi danych. RxPY obsługuje dane i zdarzenia w systemie podczas PyFunctional koncentruje się na transformacji danych przy użyciu paradygmatów programowania funkcjonalnego.
Instalowanie modułu PyFunctional
Musimy zainstalować ten moduł przed jego użyciem. Można go zainstalować za pomocą polecenia pip w następujący sposób -
pip install pyfunctional
Przykład
Poniższy przykład używa the PyFunctional moduł i jego seqklasa, która działa jako obiekt strumienia, z którym możemy iterować i manipulować. W tym programie odwzorowuje sekwencję za pomocą funkcji lamda, która podwaja każdą wartość, a następnie filtruje wartość, w której x jest większe od 4, a na koniec redukuje sekwencję do sumy wszystkich pozostałych wartości.
from functional import seq
result = seq(1,2,3).map(lambda x: x*2).filter(lambda x: x > 4).reduce(lambda x, y: x + y)
print ("Result: {}".format(result))
Wynik
Result: 6