Menu
Jest wolny
rejestracja
Dom  /  Multimedia/ Programy wielowątkowe z przykładami. Osiem prostych zasad tworzenia aplikacji wielowątkowych

Programy wielowątkowe z przykładami. Osiem prostych zasad tworzenia aplikacji wielowątkowych

Programowanie wielowątkowe nie różni się zasadniczo od pisania opartych na zdarzeniach graficznych interfejsów użytkownika, a nawet pisania prostych aplikacji sekwencyjnych. Obowiązują tu wszystkie ważne zasady dotyczące enkapsulacji, separacji obaw, luźnego sprzężenia itp. Jednak wielu programistów ma trudności z pisaniem programów wielowątkowych właśnie dlatego, że lekceważą te zasady. Zamiast tego starają się zastosować w praktyce znacznie mniej istotną wiedzę na temat wątków i prymitywów synchronizacji, zaczerpniętą z tekstów o programowaniu wielowątkowym dla początkujących.

Więc jakie są te zasady

Inny programista, mając problem, myśli: „Dokładnie, musimy zastosować wyrażenia regularne”. A teraz ma już dwa problemy – Jamie Zawiński.

Inny programista, w obliczu problemu, myśli: „O tak, użyję tutaj strumieni”. A teraz ma dziesięć problemów - Bill Schindler.

Zbyt wielu programistów, którzy podejmują się pisania kodu wielowątkowego, wpada w pułapkę, jak bohater ballady Goethego” Uczennica Maga”. Programista nauczy się tworzyć masę wątków, które w zasadzie działają, ale prędzej czy później wymykają się spod kontroli, a programista nie wie, co robić.

Ale w przeciwieństwie do porzucającego czarodzieja, nieszczęsny programista nie może liczyć na przybycie potężnego czarownika, który machnie różdżką i przywróci porządek. Zamiast tego programista sięga po najbardziej brzydkie sztuczki, próbując poradzić sobie z ciągle pojawiającymi się problemami. Rezultat jest zawsze taki sam: uzyskuje się zbyt skomplikowaną, ograniczoną, delikatną i zawodną aplikację. Ma nieustanną groźbę zakleszczenia i innych niebezpieczeństw związanych ze złym wielowątkowym kodem. Nie mówię nawet o niewyjaśnionych awariach, słabej wydajności, niekompletnych lub nieprawidłowych wynikach pracy.

Być może zastanawiałeś się: dlaczego tak się dzieje? Powszechnym błędnym przekonaniem jest: „Programowanie wielowątkowe jest bardzo trudne”. Ale tak nie jest. Jeśli program wielowątkowy jest zawodny, zwykle kończy się niepowodzeniem z tych samych powodów, co niskiej jakości program jednowątkowy. Tyle, że programista nie kieruje się podstawowymi, znanymi i sprawdzonymi metodami rozwoju. Programy wielowątkowe tylko wydają się być bardziej złożone, ponieważ im więcej równoległych wątków pójdzie nie tak, tym więcej bałaganu robią - i to znacznie szybciej niż pojedynczy wątek.

Błędne przekonanie o „złożoności programowania wielowątkowego” stało się powszechne z powodu tych programistów, którzy rozwinęli się zawodowo w pisaniu kodu jednowątkowego, po raz pierwszy zetknęli się z wielowątkowością i nie poradzili sobie z nim. Ale zamiast przemyśleć swoje uprzedzenia i nawyki związane z pracą, uparcie naprawiają fakt, że nie chcą w żaden sposób pracować. Usprawiedliwiając zawodne oprogramowanie i niedotrzymane terminy, ci ludzie powtarzają to samo: „programowanie wielowątkowe jest bardzo trudne”.

Zwróć uwagę, że powyżej mówię o typowych programach wykorzystujących wielowątkowość. Rzeczywiście, istnieją złożone scenariusze wielowątkowe – jak również złożone scenariusze jednowątkowe. Ale nie są powszechne. Z reguły w praktyce od programisty nie wymaga się niczego nadprzyrodzonego. Przenosimy dane, przekształcamy je, od czasu do czasu wykonujemy obliczenia i na koniec zapisujemy informacje w bazie danych lub wyświetlamy je na ekranie.

Nie ma nic trudnego w ulepszaniu przeciętnego programu jednowątkowego i przekształceniu go w wielowątkowy. Przynajmniej tak nie powinno być. Trudności pojawiają się z dwóch powodów:

  • programiści nie wiedzą, jak zastosować proste, dobrze znane i sprawdzone metody rozwoju;
  • większość informacji przedstawionych w książkach o programowaniu wielowątkowym jest technicznie poprawna, ale całkowicie nieprzydatna do rozwiązywania problemów aplikacyjnych.

Najważniejsze koncepcje programowania są uniwersalne. Mają one jednakowe zastosowanie do programów jednowątkowych i wielowątkowych. Programiści tonący w zamęcie strumieni po prostu nie nauczyli się ważnych lekcji, gdy opanowali kod jednowątkowy. Mogę to powiedzieć, ponieważ tacy programiści popełniają te same fundamentalne błędy w programach wielowątkowych i jednowątkowych.

Być może najważniejszą lekcją, jakiej należy się nauczyć w ciągu sześćdziesięciu lat historii programowania, jest: globalny stan zmienny- zło... Prawdziwe zło. Programy, które opierają się na globalnie mutowalnym stanie, są stosunkowo trudne do zrozumienia i generalnie zawodne, ponieważ istnieje zbyt wiele sposobów zmiany stanu. Przeprowadzono wiele badań potwierdzających tę ogólną zasadę, istnieje niezliczona ilość wzorców projektowych, których głównym celem jest wdrożenie w taki czy inny sposób ukrywania danych. Aby programy były bardziej przewidywalne, postaraj się w jak największym stopniu wyeliminować stan mutable.

W jednowątkowym programie sekwencyjnym prawdopodobieństwo uszkodzenia danych jest wprost proporcjonalne do liczby komponentów, które mogą modyfikować dane.

Z reguły nie da się całkowicie pozbyć stanu globalnego, ale programista ma w swoim arsenale bardzo skuteczne narzędzia, które pozwalają na ścisłą kontrolę, które komponenty programu mogą zmienić stan. Ponadto nauczyliśmy się tworzyć restrykcyjne warstwy API wokół prymitywnych struktur danych. Dlatego mamy dobrą kontrolę nad tym, jak zmieniają się te struktury danych.

Pod koniec lat 80. i na początku 90. problemy z globalnie mutowalnym stanem zaczęły się stopniowo ujawniać wraz z rozpowszechnieniem programowania sterowanego zdarzeniami. Programy nie zaczynały się już „od początku” ani nie podążały jedną, przewidywalną ścieżką wykonania „do końca”. Współczesne programy mają stan początkowy, po wyjściu z którego zachodzą w nich zdarzenia - w nieprzewidywalnej kolejności, ze zmiennymi odstępami czasu. Kod pozostaje jednowątkowy, ale staje się już asynchroniczny. Prawdopodobieństwo uszkodzenia danych wzrasta właśnie dlatego, że kolejność występowania zdarzeń jest bardzo ważna. Tego rodzaju sytuacje są dość powszechne: jeśli zdarzenie B nastąpi po zdarzeniu A, to wszystko działa dobrze. Ale jeśli zdarzenie A nastąpi po zdarzeniu B, a zdarzenie C ma czas na interweniowanie między nimi, dane mogą zostać zniekształcone nie do poznania.

Jeśli w grę wchodzą strumienie równoległe, problem jest jeszcze bardziej zaostrzony, ponieważ kilka metod może jednocześnie operować na stanie globalnym. Niemożliwe staje się oszacowanie, jak dokładnie zmienia się stan globalny. Mówimy już nie tylko o tym, że zdarzenia mogą zachodzić w nieprzewidywalnej kolejności, ale także o tym, że stan kilku wątków wykonania może być aktualizowany. jednocześnie... Dzięki programowaniu asynchronicznemu można co najmniej zapewnić, że pewne zdarzenie nie może nastąpić przed zakończeniem przetwarzania innego zdarzenia. Oznacza to, że można z całą pewnością powiedzieć, jaki będzie stan globalny po zakończeniu przetwarzania określonego zdarzenia. W kodzie wielowątkowym z reguły nie da się powiedzieć, które zdarzenia będą miały miejsce równolegle, więc nie da się z całą pewnością opisać stanu globalnego w dowolnym momencie.

Program wielowątkowy z rozległym stanem globalnie mutowalnym jest jednym z najbardziej wymownych przykładów zasady nieoznaczoności Heisenberga, jakie znam. Nie można sprawdzić stanu programu bez zmiany jego zachowania.

Kiedy zaczynam kolejną filipikę o globalnym stanie mutowalnym (istotę opisano w kilku poprzednich akapitach), programiści przewracają oczami i zapewniają mnie, że wiedzieli o tym od dawna. Ale jeśli to wiesz, dlaczego nie możesz tego rozpoznać po swoim kodzie? Programy są przepełnione globalnym stanem mutowalnym, a programiści zastanawiają się, dlaczego kod nie działa.

Nic dziwnego, że najważniejsza praca w programowaniu wielowątkowym ma miejsce w fazie projektowania. Wymagane jest jasne zdefiniowanie, co program ma robić, opracowanie niezależnych modułów do realizacji wszystkich funkcji, szczegółowe opisanie jakie dane są wymagane dla którego modułu oraz określenie sposobów wymiany informacji między modułami ( Tak, nie zapomnij przygotować ładnych koszulek dla wszystkich zaangażowanych w projekt. Pierwsza rzecz.- około. wyd. w oryginale). Proces ten nie różni się zasadniczo od projektowania programu jednowątkowego. Kluczem do sukcesu, podobnie jak w przypadku kodu jednowątkowego, jest ograniczenie interakcji między modułami. Jeśli możesz pozbyć się współdzielonego stanu mutowalnego, problemy z udostępnianiem danych po prostu się nie pojawią.

Ktoś mógłby argumentować, że czasami nie ma czasu na tak delikatną konstrukcję programu, która pozwoli obejść się bez globalnego państwa. Uważam, że można i trzeba poświęcić na to czas. Nic nie wpływa na programy wielowątkowe tak destrukcyjnie, jak próba radzenia sobie z globalnym stanem mutowalnym. Im więcej szczegółów musisz zarządzać, tym większe prawdopodobieństwo, że program osiągnie szczyt i awarię.

W realistycznych aplikacjach musi istnieć jakiś wspólny stan, który może się zmienić. I tutaj większość programistów zaczyna mieć problemy. Programista widzi, że wymagany jest tutaj stan współdzielony, zwraca się do wielowątkowego arsenału i bierze stamtąd najprostsze narzędzie: uniwersalny zamek (sekcja krytyczna, mutex, czy jakkolwiek to nazywają). Wydaje się, że wierzą, że wzajemne wykluczenie rozwiąże wszystkie problemy związane z udostępnianiem danych.

Liczba problemów, które mogą się pojawić w przypadku takiego pojedynczego zamka, jest oszałamiająca. Należy zwrócić uwagę na warunki wyścigu, problemy z bramkowaniem z nadmiernie rozbudowanym blokowaniem oraz kwestie sprawiedliwości alokacji to tylko kilka przykładów. Jeśli masz wiele blokad, zwłaszcza jeśli są zagnieżdżone, musisz również podjąć środki przeciwko zakleszczeniu, dynamicznemu blokowaniu, blokowaniu kolejek i innym zagrożeniom związanym ze współbieżnością. Ponadto istnieją również nieodłączne problemy związane z blokowaniem pojedynczych.
Kiedy piszę lub recenzuję kod, mam prawie niezawodną żelazną zasadę: jeśli zrobiłeś zamek, to wygląda na to, że gdzieś popełniłeś błąd.

To stwierdzenie można skomentować na dwa sposoby:

  1. Jeśli potrzebujesz blokowania, prawdopodobnie masz globalny stan mutowalny, który chcesz chronić przed współbieżnymi aktualizacjami. Obecność globalnego stanu zmiennego jest wadą w fazie projektowania aplikacji. Przejrzyj i przeprojektuj.
  2. Prawidłowe korzystanie z blokad nie jest łatwe, a wyizolowanie błędów blokowania może być niezwykle trudne. Jest bardzo prawdopodobne, że będziesz używał zamka niewłaściwie. Jeśli widzę blokadę, a program zachowuje się nietypowo, to najpierw sprawdzam kod zależny od blokady. I zwykle znajduję w tym problemy.

Obie te interpretacje są poprawne.

Pisanie kodu wielowątkowego jest łatwe. Ale bardzo, bardzo trudno jest poprawnie używać prymitywów synchronizacji. Być może nie masz kwalifikacji do prawidłowego korzystania z choćby jednego zamka. W końcu zamki i inne prymitywy synchronizacji to konstrukcje wznoszone na poziomie całego systemu. Ludzie, którzy rozumieją programowanie równoległe znacznie lepiej niż ty, używają tych prymitywów do budowania współbieżnych struktur danych i konstrukcji synchronizacji wysokiego poziomu. A ty i ja, zwykli programiści, bierzemy takie konstrukcje i używamy ich w naszym kodzie. Programista aplikacji nie powinien częściej używać prymitywów synchronizacji niskiego poziomu niż bezpośrednie wywołania sterowników urządzeń. To znaczy prawie nigdy.

Próba użycia zamków do rozwiązania problemów z udostępnianiem danych jest jak gaszenie ognia ciekłym tlenem. Podobnie jak w przypadku pożaru, takim problemom łatwiej zapobiegać niż naprawiać. Jeśli pozbędziesz się stanu współdzielonego, nie musisz też nadużywać prymitywów synchronizacji.

Większość tego, co wiesz o wielowątkowości, jest nieistotna

W samouczkach dotyczących wielowątkowości dla początkujących dowiesz się, czym są wątki. Następnie autor zacznie rozważać różne sposoby, w jakie te wątki mogą działać równolegle – na przykład porozmawia o kontrolowaniu dostępu do współdzielonych danych za pomocą blokad i semaforów oraz zastanowi się, co może się zdarzyć podczas pracy ze zdarzeniami. Przyjrzymy się bliżej zmiennym warunkującym, barierom pamięci, sekcjom krytycznym, muteksom, polam lotnym i operacjom atomowym. Omówione zostaną przykłady wykorzystania tych niskopoziomowych konstrukcji do wykonywania wszelkiego rodzaju operacji systemowych. Po przeczytaniu tego materiału do połowy programista stwierdza, że ​​wie już wystarczająco dużo o tych wszystkich prymitywach i ich zastosowaniu. W końcu, jeśli wiem, jak to działa na poziomie systemu, mogę to zastosować w ten sam sposób na poziomie aplikacji. Tak?

Wyobraź sobie, że mówisz nastolatkowi, jak samodzielnie złożyć silnik spalinowy. Następnie, bez żadnego treningu w prowadzeniu samochodu, sadzasz go za kierownicą samochodu i mówisz „Jedź!” Nastolatek rozumie, jak działa samochód, ale nie ma pojęcia, jak się nim dostać z punktu A do punktu B.

Zrozumienie, jak działają wątki na poziomie systemu, zwykle nie pomaga w żaden sposób na poziomie aplikacji. Nie sugeruję, że programiści nie muszą uczyć się tych wszystkich niskopoziomowych szczegółów. Po prostu nie oczekuj, że będziesz w stanie od razu zastosować tę wiedzę podczas projektowania lub tworzenia aplikacji biznesowej.

Literatura wprowadzająca do wątków (i powiązane kursy akademickie) nie powinna badać takich niskopoziomowych konstrukcji. Musisz skupić się na rozwiązywaniu najczęstszych klas problemów i pokazać programistom, jak te problemy są rozwiązywane przy użyciu funkcji wysokiego poziomu. W zasadzie większość aplikacji biznesowych to niezwykle proste programy. Odczytują dane z jednego lub więcej urządzeń wejściowych, wykonują na nich złożone przetwarzanie (na przykład w procesie żądają więcej danych), a następnie wyprowadzają wyniki.

Często takie programy idealnie pasują do modelu dostawca-konsument, który wymaga tylko trzech wątków:

  • strumień wejściowy odczytuje dane i umieszcza je w kolejce wejściowej;
  • wątek roboczy odczytuje rekordy z kolejki wejściowej, przetwarza je i umieszcza wyniki w kolejce wyjściowej;
  • strumień wyjściowy odczytuje wpisy z kolejki wyjściowej i przechowuje je.

Te trzy wątki działają niezależnie, komunikacja między nimi odbywa się na poziomie kolejki.

Chociaż technicznie kolejki te można traktować jako strefy współdzielonego stanu, w praktyce są to tylko kanały komunikacyjne, w których działa ich własna wewnętrzna synchronizacja. Kolejki obsługują pracę z wieloma producentami i konsumentami jednocześnie, a jednocześnie można dodawać i usuwać do nich elementy.

Ponieważ etapy wejściowe, przetwarzania i wyjściowe są od siebie odizolowane, ich implementację można łatwo zmienić bez wpływu na resztę programu. Dopóki typ danych w kolejce nie ulegnie zmianie, możesz według własnego uznania dokonać refaktoryzacji poszczególnych komponentów programu. Dodatkowo, ponieważ w kolejce uczestniczy dowolna liczba dostawców i konsumentów, nie jest trudno dodać kolejnych producentów/odbiorców. Możemy mieć dziesiątki strumieni wejściowych zapisujących informacje do tej samej kolejki lub dziesiątki wątków roboczych pobierających informacje z kolejki wejściowej i przetwarzających dane. W ramach jednego komputera taki model dobrze się skaluje.

Co najważniejsze, nowoczesne języki programowania i biblioteki bardzo ułatwiają tworzenie aplikacji producent-konsument. W .NET znajdziesz kolekcje równoległe i bibliotekę przepływu danych TPL. Java ma usługę Executor, a także BlockingQueue i inne klasy z przestrzeni nazw java.util.concurrent. C ++ ma bibliotekę wątków Boost i bibliotekę Thread Building Blocks firmy Intel. Microsoft Visual Studio 2013 wprowadza agentów asynchronicznych. Podobne biblioteki są również dostępne w Pythonie, JavaScript, Ruby, PHP oraz, o ile wiem, w wielu innych językach. Możesz stworzyć aplikację producent-konsument przy użyciu dowolnego z tych pakietów, bez konieczności uciekania się do blokad, semaforów, zmiennych warunkowych lub jakichkolwiek innych prymitywów synchronizacji.

W tych bibliotekach można swobodnie używać szerokiej gamy prymitywów synchronizacji. Jest okej. Wszystkie te biblioteki są pisane przez ludzi, którzy rozumieją wielowątkowość nieporównywalnie lepiej niż przeciętny programista. Praca z taką biblioteką jest praktycznie taka sama, jak korzystanie z biblioteki języka uruchomieniowego. Można to porównać do programowania w języku wysokiego poziomu, a nie w języku asemblera.

Model dostawca-konsument to tylko jeden z wielu przykładów. Powyższe biblioteki zawierają klasy, których można użyć do zaimplementowania wielu typowych wzorców projektowania wątków bez wchodzenia w szczegóły niskiego poziomu. Możliwe jest tworzenie wielowątkowych aplikacji na dużą skalę bez martwienia się o to, jak wątki są koordynowane i synchronizowane.

Praca z bibliotekami

Tak więc tworzenie programów wielowątkowych nie różni się zasadniczo od pisania jednowątkowych programów synchronicznych. Ważne zasady enkapsulacji i ukrywania danych są uniwersalne i zyskują na znaczeniu tylko wtedy, gdy zaangażowanych jest wiele współbieżnych wątków. Jeśli zlekceważysz te ważne aspekty, nawet najbardziej wszechstronna wiedza na temat wątków niskiego poziomu Cię nie uratuje.

Współcześni programiści muszą rozwiązać wiele problemów na poziomie programowania aplikacji, zdarza się, że po prostu nie ma czasu na myślenie o tym, co dzieje się na poziomie systemu. Im bardziej skomplikowane aplikacje, tym bardziej złożone szczegóły muszą być ukryte pomiędzy poziomami API. Robimy to od kilkunastu lat. Można argumentować, że jakościowe ukrywanie przed programistą złożoności systemu jest głównym powodem, dla którego programista jest w stanie pisać nowoczesne aplikacje. Czy nie ukrywamy złożoności systemu, implementując pętlę komunikatów interfejsu użytkownika, budując protokoły komunikacyjne niskiego poziomu itp.?

Podobnie jest z wielowątkowością. Większość wielowątkowych scenariuszy, z którymi może się spotkać przeciętny programista aplikacji biznesowych, jest już dobrze znana i dobrze zaimplementowana w bibliotekach. Funkcje biblioteczne świetnie radzą sobie z ukrywaniem przytłaczającej złożoności paralelizmu. Musisz nauczyć się korzystać z tych bibliotek w taki sam sposób, w jaki używasz bibliotek elementów interfejsu użytkownika, protokołów komunikacyjnych i wielu innych narzędzi, które po prostu działają. Niskopoziomową wielowątkowość zostaw specjalistom – autorom bibliotek wykorzystywanych przy tworzeniu aplikacji.

koniec pliku. W ten sposób wpisy dziennika utworzone przez różne procesy nigdy nie są mieszane. Bardziej nowoczesne systemy Unix udostępniają specjalną usługę syslog (3C) do logowania.

Zalety:

  1. Łatwość rozwoju. W rzeczywistości uruchamiamy wiele kopii pojedynczej aplikacji wątkowej i działają one niezależnie od siebie. Możliwe jest nieużywanie żadnego konkretnego wielowątkowego API i środki komunikacji międzyprocesowej.
  2. Wysoka niezawodność. Nieprawidłowe zakończenie któregokolwiek z procesów nie wpływa w żaden sposób na pozostałe procesy.
  3. Dobra przenośność. Aplikacja będzie działać na każdym wielozadaniowy system operacyjny
  4. Wysoki poziom bezpieczeństwa. Różne procesy aplikacji mogą działać w imieniu różnych użytkowników. W ten sposób możliwa jest realizacja zasady najmniejszych uprawnień, gdy każdy z procesów ma tylko te uprawnienia, które są mu niezbędne do działania. Nawet w przypadku wykrycia błędu w jednym z procesów umożliwiających zdalne wykonanie kodu, atakujący będzie mógł uzyskać jedynie poziom dostępu, z jakim ten proces został wykonany.

Niedogodności:

  1. Nie wszystkie aplikacje mogą być dostarczone w ten sposób. Na przykład ta architektura jest odpowiednia dla serwera obsługującego statyczne strony HTML, ale wcale nie dla serwera bazy danych i wielu serwerów aplikacji.
  2. Tworzenie i niszczenie procesów jest kosztowną operacją, dlatego taka architektura nie jest optymalna dla wielu zadań.

Systemy uniksowe podejmują różne kroki, aby tworzenie procesu i uruchamianie nowego programu w procesie było jak najtańsze. Musisz jednak zrozumieć, że tworzenie wątku w ramach istniejącego procesu zawsze będzie tańsze niż tworzenie nowego procesu.

Przykłady: apache 1.x (serwer HTTP)

Aplikacje wieloprocesowe komunikujące się przez gniazda, potoki i kolejki komunikatów IPC Systemu V

Wymienione środki IPC (komunikacja międzyprocesowa) należą do tzw. środków harmonicznej komunikacji międzyprocesowej. Pozwalają organizować interakcję procesów i wątków bez korzystania z pamięci współdzielonej. Teoretycy programowania bardzo lubią tę architekturę, ponieważ praktycznie eliminuje wiele opcji błędów konkurencji.

Zalety:

  1. Względna łatwość rozwoju.
  2. Wysoka niezawodność. Nieprawidłowe zakończenie jednego z procesów powoduje zamknięcie potoku lub gniazda, aw przypadku kolejek komunikatów komunikaty przestają wchodzić lub pobierać z kolejki. Reszta procesów aplikacji może łatwo wykryć ten błąd i naprawić go, być może (ale niekoniecznie) po prostu ponownie uruchamiając nieudany proces.
  3. Wiele z tych aplikacji (zwłaszcza opartych na gniazdach) można łatwo przeprojektować, aby działały w środowisku rozproszonym, w którym różne składniki aplikacji działają na różnych komputerach.
  4. Dobra przenośność. Aplikacja będzie działać na większości wielozadaniowych systemów operacyjnych, w tym na starszych systemach Unix.
  5. Wysoki poziom bezpieczeństwa. Różne procesy aplikacji mogą działać w imieniu różnych użytkowników. W ten sposób możliwa jest realizacja zasady najmniejszych uprawnień, gdy każdy z procesów ma tylko te uprawnienia, które są mu niezbędne do działania.

Nawet w przypadku wykrycia błędu w jednym z procesów umożliwiających zdalne wykonanie kodu, atakujący będzie mógł uzyskać jedynie poziom dostępu, z jakim ten proces został wykonany.

Niedogodności:

  1. Ta architektura nie jest łatwa do zaprojektowania i wdrożenia we wszystkich aplikacjach.
  2. Wszystkie wymienione typy narzędzi IPC zakładają szeregową transmisję danych. Jeśli wymagany jest losowy dostęp do współdzielonych danych, ta architektura jest niewygodna.
  3. Przesyłanie danych przez potok, gniazdo i kolejkę komunikatów wymaga wykonania wywołań systemowych, a dane są kopiowane dwukrotnie — najpierw z oryginalnej przestrzeni adresowej procesu do przestrzeni adresowej jądra, a następnie z przestrzeni adresowej jądra do pamięci proces docelowy... Są to kosztowne operacje. Podczas przesyłania dużych ilości danych może to stać się poważnym problemem.
  4. Większość systemów ma ograniczenia dotyczące całkowitej liczby rur, gniazd i urządzeń IPC. Na przykład Solaris domyślnie dopuszcza maksymalnie 1024 otwartych potoków, gniazd i plików na proces (jest to spowodowane ograniczeniami wywołania systemowego select). Limit architektoniczny Solarisa to 65 536 potoków, gniazd i plików na proces.

    Limit całkowitej liczby gniazd TCP/IP wynosi nie więcej niż 65536 na interfejs sieciowy (ze względu na format nagłówków TCP). Kolejki komunikatów IPC Systemu V znajdują się w przestrzeni adresowej jądra, dlatego istnieją sztywne ograniczenia liczby kolejek w systemie oraz liczby i liczby komunikatów jednocześnie umieszczanych w kolejce.

  5. Tworzenie i niszczenie procesu oraz przełączanie między procesami to kosztowne operacje. Ta architektura nie jest optymalna we wszystkich przypadkach.

Aplikacje wieloprocesowe w pamięci współdzielonej

Pamięć współdzielona może być pamięcią współdzieloną IPC Systemu V i mapowaniem plików do pamięci. Aby zsynchronizować dostęp, można użyć semaforów IPC Systemu V, muteksów i semaforów POSIX, a podczas mapowania plików w pamięci przechwycić sekcje pliku.

Zalety:

  1. Wydajny losowy dostęp do udostępnionych danych. Ta architektura jest odpowiednia do implementacji serwerów baz danych.
  2. Wysoka tolerancja. Może być przeniesiony do dowolnego systemu operacyjnego, który obsługuje lub emuluje IPC Systemu V.
  3. Stosunkowo wysokie bezpieczeństwo. Różne procesy aplikacji mogą działać w imieniu różnych użytkowników. W ten sposób możliwa jest realizacja zasady najmniejszych uprawnień, gdy każdy z procesów ma tylko te uprawnienia, które są mu niezbędne do działania. Jednak rozdział poziomów dostępu nie jest tak rygorystyczny, jak w omawianych wcześniej architekturach.

Niedogodności:

  1. Względna złożoność rozwoju. Błędy synchronizacji dostępu – tak zwane błędy konkurencji – są bardzo trudne do wykrycia podczas testowania.

    Może to skutkować 3-5-krotnym wzrostem całkowitego kosztu rozwoju w porównaniu z architekturą jednowątkową lub prostszą architekturą wielozadaniową.

  2. Niska niezawodność. Nieprawidłowe zakończenie dowolnego procesu aplikacji może pozostawić (i często pozostawia) pamięć współdzieloną w niespójnym stanie.

    To często powoduje awarię reszty aplikacji. Niektóre aplikacje, takie jak Lotus Domino, celowo zabijają procesy obejmujące cały serwer, jeśli którykolwiek z nich zostanie nieprawidłowo zakończony.

  3. Tworzenie i niszczenie procesu oraz przełączanie się między nimi to kosztowne operacje.

    Dlatego ta architektura nie jest optymalna dla wszystkich aplikacji.

  4. W pewnych okolicznościach korzystanie z pamięci współdzielonej może prowadzić do eskalacji uprawnień. Jeśli w jednym z procesów zostanie znaleziony błąd, który prowadzi do zdalnego wykonania kodu, jest wysoce prawdopodobne, że osoba atakująca będzie mogła użyć go do zdalnego wykonania kodu w innych procesach aplikacji.

    Oznacza to, że w najgorszym przypadku atakujący może uzyskać poziom dostępu odpowiadający najwyższemu poziomowi dostępu do procesów aplikacji.

  5. Aplikacje pamięci współdzielonej muszą działać na tym samym komputerze fizycznym lub przynajmniej na maszynach, które mają współdzieloną pamięć RAM. W rzeczywistości to ograniczenie można obejść, na przykład za pomocą plików współdzielonych mapowanych w pamięci, ale powoduje to znaczne obciążenie.

W rzeczywistości ta architektura łączy wady właściwych aplikacji wieloprocesorowych i wielowątkowych. Jednak wiele popularnych aplikacji opracowanych w latach 80. i wczesnych 90., przed ustandaryzowanymi przez Uniksa wielowątkowymi API, korzysta z tej architektury. Jest to wiele serwerów bazodanowych, zarówno komercyjnych (Oracle, DB2, Lotus Domino), jak i darmowych, nowoczesnych wersji Sendmaila i kilku innych serwerów pocztowych.

Właściwe aplikacje wielowątkowe

Wątki lub wątki aplikacji działają w ramach jednego procesu. Cała przestrzeń adresowa procesu jest współdzielona przez wątki. Na pierwszy rzut oka wydaje się, że pozwala to organizować interakcję między wątkami bez żadnych specjalnych interfejsów API. W rzeczywistości tak nie jest – jeśli kilka wątków pracuje ze wspólną strukturą danych lub zasobem systemowym i przynajmniej jeden z wątków modyfikuje tę strukturę, to w pewnym momencie dane będą niespójne.

Dlatego wątki muszą używać specjalnych środków do organizowania interakcji. Najważniejszymi narzędziami są prymitywy wzajemnego wykluczania (muteksy i blokady odczytu/zapisu). Używając tych prymitywów, programista może zapewnić, że żadne wątki nie będą miały dostępu do współdzielonych zasobów, gdy są one w niespójnym stanie (nazywa się to wzajemnym wykluczaniem). IPC Systemu V, współdzielone są tylko te struktury, które znajdują się w segmencie pamięci współdzielonej. Zmienne regularne i normalnie przydzielone dynamiczne struktury danych są różne dla każdego procesu). Błędy w dostępie do wspólnych danych – błędy konkurencji – są bardzo trudne do wykrycia podczas testów.

  • Wysokie koszty tworzenia i debugowania aplikacji ze względu na klauzulę 1.
  • Niska niezawodność. Zniszczenie struktur danych, na przykład poprzez przepełnienie bufora lub błędy wskaźnika, wpływa na wszystkie wątki w procesie i zwykle skutkuje nieprawidłowym zakończeniem całego procesu. Inne błędy krytyczne, takie jak dzielenie przez zero w jednym z wątków, również zwykle powodują awarię wszystkich wątków w procesie.
  • Niskie bezpieczeństwo. Wszystkie wątki aplikacji działają w jednym procesie, to znaczy w imieniu tego samego użytkownika i z tymi samymi prawami dostępu. Nie jest możliwe zrealizowanie zasady minimalnych niezbędnych uprawnień, proces musi być wykonywany w imieniu użytkownika, który może wykonać wszystkie operacje wymagane przez wszystkie wątki aplikacji.
  • Tworzenie wątku to wciąż dość kosztowna operacja. Dla każdego wątku koniecznie przydzielany jest własny stos, który domyślnie zajmuje 1 megabajt pamięci RAM w architekturach 32-bitowych i 2 megabajty w architekturach 64-bitowych oraz kilka innych zasobów. Dlatego ta architektura nie jest optymalna dla wszystkich aplikacji.
  • Brak możliwości uruchomienia aplikacji na wielomaszynowym systemie obliczeniowym. Techniki wymienione w poprzedniej sekcji, takie jak mapowanie udostępnionych plików do pamięci, nie mają zastosowania do programu wielowątkowego.
  • Ogólnie można powiedzieć, że aplikacje wielowątkowe mają prawie te same zalety i wady, co aplikacje wieloprocesorowe, które korzystają z pamięci współdzielonej.

    Jednak koszt uruchomienia aplikacji wielowątkowej jest niższy, a tworzenie takiej aplikacji jest pod pewnymi względami łatwiejsze niż aplikacji opartej na pamięci współdzielonej. Dlatego w ostatnich latach coraz większą popularnością cieszą się aplikacje wielowątkowe.

    Rozdział 10.

    Aplikacje wielowątkowe

    Wielozadaniowość w nowoczesnych systemach operacyjnych jest oczywista [ Przed pojawieniem się Apple OS X na komputerach Macintosh nie było nowoczesnych wielozadaniowych systemów operacyjnych. Bardzo trudno jest poprawnie zaprojektować system operacyjny z pełną wielozadaniowością, więc OS X musiał być oparty na Unixie.]. Użytkownik oczekuje, że przy równoczesnym uruchomieniu edytora tekstu i klienta pocztowego programy te nie będą powodować konfliktów, a przy odbiorze poczty e-mail edytor nie przestanie działać. Gdy kilka programów jest uruchamianych jednocześnie, system operacyjny szybko przełącza się między programami, udostępniając im po kolei procesor (chyba że na komputerze jest zainstalowanych wiele procesorów). W rezultacie, iluzja uruchamianie wielu programów jednocześnie, ponieważ nawet najlepsza maszynistka (i najszybsze połączenie internetowe) nie nadąża za nowoczesnym procesorem.

    W pewnym sensie wielowątkowość może być postrzegana jako kolejny poziom wielozadaniowości: zamiast przełączania się między różnymi programy system operacyjny przełącza się między różnymi częściami tego samego programu. Na przykład wielowątkowy klient poczty e-mail umożliwia odbieranie nowych wiadomości e-mail podczas czytania lub tworzenia nowych wiadomości. W dzisiejszych czasach wielowątkowość jest również uważana za pewnik przez wielu użytkowników.

    VB nigdy nie miał normalnej obsługi wielowątkowości. To prawda, że ​​​​jedna z jego odmian pojawiła się w VB5 - model wspólnego przesyłania strumieniowego(wątki w mieszkaniu). Jak wkrótce zobaczysz, model współpracy zapewnia programiście niektóre z korzyści płynących z wielowątkowości, ale nie wykorzystuje tego w pełni. Prędzej czy później trzeba będzie przenieść się z maszyny szkoleniowej na prawdziwą, a VB .NET stało się pierwszą wersją VB z obsługą darmowego modelu wielowątkowego.

    Jednak wielowątkowość nie jest jedną z funkcji, którą można łatwo zaimplementować w językach programowania i łatwo opanować przez programistów. Czemu?

    Ponieważ w aplikacjach wielowątkowych mogą wystąpić bardzo trudne błędy, które pojawiają się i znikają nieprzewidywalnie (a takie błędy są najtrudniejsze do debugowania).

    Szczerze mówiąc: wielowątkowość to jeden z najtrudniejszych obszarów programowania. Najmniejsza nieuwaga prowadzi do pojawienia się nieuchwytnych błędów, których korekta wymaga astronomicznych sum. Z tego powodu ten rozdział zawiera wiele zły przykłady - celowo napisaliśmy je w taki sposób, aby pokazać typowe błędy. Jest to najbezpieczniejsze podejście do nauki programowania wielowątkowego: musisz być w stanie dostrzec potencjalne problemy, gdy na pierwszy rzut oka wszystko wydaje się działać dobrze, i wiedzieć, jak je rozwiązać. Jeśli chcesz korzystać z wielowątkowych technik programowania, nie możesz się bez tego obejść.

    Ten rozdział położy solidne podstawy do dalszej samodzielnej pracy, ale nie będziemy w stanie opisać programowania wielowątkowego we wszystkich zawiłościach - jedynie drukowana dokumentacja dotycząca klas przestrzeni nazw Threading zajmuje więcej niż 100 stron. Jeśli chcesz opanować programowanie wielowątkowe na wyższym poziomie, zajrzyj do specjalistycznych książek.

    Jednak bez względu na to, jak niebezpieczne jest programowanie wielowątkowe, jest ono niezbędne do profesjonalnego rozwiązywania niektórych problemów. Jeśli twoje programy nie używają wielowątkowości tam, gdzie jest to właściwe, użytkownicy będą bardzo sfrustrowani i będą preferować inny produkt. Na przykład dopiero w czwartej wersji popularnego programu pocztowego Eudora pojawiły się możliwości wielowątkowości, bez których nie sposób sobie wyobrazić nowoczesnego programu pocztowego. Zanim Eudora wprowadziła obsługę wielowątkowości, wielu użytkowników (w tym jeden z autorów tej książki) przeszło na inne produkty.

    Wreszcie w .NET programy jednowątkowe po prostu nie istnieją. Wszystko Programy .NET są wielowątkowe, ponieważ moduł odśmiecania pamięci działa w tle jako proces o niskim priorytecie. Jak pokazano poniżej, w przypadku poważnego programowania graficznego w .NET, prawidłowe wątkowanie może zapobiec blokowaniu interfejsu graficznego, gdy program wykonuje długie operacje.

    Przedstawiamy wielowątkowość

    Każdy program działa w konkretnym kontekst, opisywanie dystrybucji kodu i danych w pamięci. Podczas zapisywania kontekstu faktycznie zapisywany jest stan przepływu programu, co pozwala na jego przywrócenie w przyszłości i kontynuowanie wykonywania programu.

    Zapisywanie kontekstu wiąże się z kosztem czasu i pamięci. System operacyjny zapamiętuje stan wątku programu i przekazuje kontrolę do innego wątku. Gdy program chce kontynuować wykonywanie zawieszonego wątku, należy przywrócić zapisany kontekst, co trwa jeszcze dłużej. Dlatego wielowątkowość należy stosować tylko wtedy, gdy korzyści równoważą wszystkie koszty. Poniżej wymieniono kilka typowych przykładów.

    • Funkcjonalność programu jest jasno i naturalnie podzielona na kilka heterogenicznych operacji, jak na przykład z odbieraniem e-maili i przygotowywaniem nowych wiadomości.
    • Program wykonuje długie i złożone obliczenia, a nie chcesz, aby interfejs graficzny był blokowany na czas obliczeń.
    • Program działa na komputerze wieloprocesorowym z systemem operacyjnym obsługującym wiele procesorów (o ile liczba aktywnych wątków nie przekracza liczby procesorów, wykonywanie równoległe jest praktycznie wolne od kosztów związanych z przełączaniem wątków).

    Zanim przejdziemy do mechaniki programów wielowątkowych, należy zwrócić uwagę na jedną okoliczność, która często powoduje zamieszanie wśród początkujących w dziedzinie programowania wielowątkowego.

    W przebiegu programu wykonywana jest procedura, a nie obiekt.

    Trudno powiedzieć, co rozumie się przez wyrażenie „obiekt w biegu”, ale jeden z autorów często prowadzi seminaria dotyczące wielowątkowości i to pytanie zadawane jest częściej niż inni. Być może ktoś myśli, że praca wątku programu zaczyna się od wywołania metody New klasy, po czym wątek przetwarza wszystkie komunikaty przekazane do odpowiedniego obiektu. Takie reprezentacje absolutnie jest źle. Jeden obiekt może zawierać kilka wątków, które wykonują różne (a czasem nawet te same) metody, podczas gdy komunikaty obiektu są przesyłane i odbierane przez kilka różnych wątków (swoją drogą jest to jeden z powodów, które komplikują programowanie wielowątkowe: aby debugować program, musisz dowiedzieć się, który wątek w danym momencie wykonuje tę lub inną procedurę!).

    Ponieważ wątki są tworzone z metod obiektów, sam obiekt jest zwykle tworzony przed wątkiem. Po pomyślnym utworzeniu obiektu program tworzy wątek, przekazując mu adres metody obiektu i dopiero potem wydaje polecenie rozpoczęcia wykonywania wątku. Procedura, dla której utworzono wątek, podobnie jak wszystkie procedury, może tworzyć nowe obiekty, wykonywać operacje na istniejących obiektach oraz wywoływać inne procedury i funkcje, które znajdują się w jej zakresie.

    Typowe metody klas mogą być również wykonywane w wątkach programu. W tym przypadku należy również pamiętać o innej ważnej okoliczności: wątek kończy się wyjściem z procedury, dla której został utworzony. Normalne zakończenie przebiegu programu nie jest możliwe do momentu zakończenia procedury.

    Wątki mogą kończyć się nie tylko naturalnie, ale także nienormalnie. Generalnie nie jest to zalecane. Zobacz Zakańczanie i przerywanie strumieni, aby uzyskać więcej informacji.

    Podstawowe narzędzia .NET związane z wykorzystaniem wątków programu są skoncentrowane w przestrzeni nazw Threading. Dlatego większość programów wielowątkowych powinna zaczynać się od następującego wiersza:

    Importuje system. Threading

    Importowanie przestrzeni nazw ułatwia pisanie programu i włącza technologię IntelliSense.

    Bezpośrednie powiązanie przepływów z procedurami sugeruje, że na tym obrazie delegaci(patrz rozdział 6). W szczególności przestrzeń nazw Threading zawiera delegata ThreadStart, który jest zwykle używany podczas uruchamiania wątków programu. Składnia korzystania z tego delegata wygląda następująco:

    Delegat publiczny Sub ThreadStart ()

    Kod wywoływany za pomocą delegata ThreadStart nie może mieć parametrów ani wartości zwracanej, więc nie można tworzyć wątków dla funkcji (które zwracają wartość) i procedur z parametrami. Aby przesłać informacje ze strumienia, trzeba również poszukać alternatywnych środków, ponieważ wykonywane metody nie zwracają wartości i nie mogą korzystać z transferu przez referencję. Na przykład, jeśli ThreadMethod znajduje się w klasie WilluseThread, to ThreadMethod może przekazywać informacje, modyfikując właściwości wystąpień klasy WillUseThread.

    Domeny aplikacji

    Wątki .NET działają w tak zwanych domenach aplikacji, definiowanych w dokumentacji jako „piaskownica, w której działa aplikacja”. Domena aplikacji może być traktowana jako uproszczona wersja procesów Win32; pojedynczy proces Win32 może zawierać wiele domen aplikacji. Główna różnica między domenami aplikacji a procesami polega na tym, że proces Win32 ma własną przestrzeń adresową (w dokumentacji domeny aplikacji są również porównywane z procesami logicznymi działającymi w procesie fizycznym). W NET całe zarządzanie pamięcią jest obsługiwane przez środowisko wykonawcze, więc wiele domen aplikacji może działać w jednym procesie Win32. Jedną z korzyści tego schematu są ulepszone możliwości skalowania aplikacji. Narzędzia do pracy z domenami aplikacji znajdują się w klasie AppDomain. Zalecamy zapoznanie się z dokumentacją dla tej klasy. Z jego pomocą możesz uzyskać informacje o środowisku, w którym działa Twój program. W szczególności klasa AppDomain jest używana podczas wykonywania odbicia na klasach systemu .NET. Poniższy program wyświetla listę załadowanych zespołów.

    Importuje system.Reflection

    Moduł Modułl

    Sub Główny ()

    Przyciemnij domenę jako domenę aplikacji

    theDomain = AppDomain.Bieżąca domena

    Zespoły przyciemniania () As

    Zespoły = theDomain.GetAssemblies

    Dim anAssemblyxAs

    Dla każdego zespołu w zespołach

    Console.WriteLinetanAssembly.Full Name) Dalej

    Konsola.ReadLine()

    Napis końcowy

    Moduł końcowy

    Tworzenie strumieni

    Zacznijmy od szczątkowego przykładu. Załóżmy, że chcesz uruchomić procedurę w osobnym wątku, która zmniejsza wartość licznika w nieskończonej pętli. Procedura jest zdefiniowana jako część klasy:

    Klasa publiczna użyje wątków

    Publiczne Odejmowanie OdLicznika ()

    Licznik dim As Integer

    Dopóki liczba rzeczywista - = 1

    Console.WriteLlne ("Jestem w innym wątku i licznik ="

    & liczyć)

    Pętla

    Napis końcowy

    Koniec klasy

    Ponieważ warunek pętli Do jest zawsze prawdziwy, możesz pomyśleć, że nic nie będzie kolidować z procedurą SubtractFromCounter. Jednak w aplikacji wielowątkowej nie zawsze tak jest.

    Poniższy fragment kodu przedstawia procedurę Sub Main, która uruchamia wątek, oraz polecenie Imports:

    Opcja ściśle przy imporcie modułu System.Threading Module

    Sub Główny ()

    1 Przyciemnij myTest jako nowy WillUseThreads ()

    2 Dim bThreadStart jako nowy początek wątku (AdresOf _

    mójTest.OdejmijOdLicznika)

    3 Przyciemnij bThread jako nowy wątek (bThreadStart)

    4 "bWątek.Start ()

    Dim i jako liczba całkowita

    5 Rób póki prawda

    Console.WriteLine ("W głównym wątku i liczba jest" & i) i + = 1

    Pętla

    Napis końcowy

    Moduł końcowy

    Przyjrzyjmy się kolejno najważniejszym punktom. Przede wszystkim procedura Sub Man n zawsze działa w główny strumień(główny wątek). W programach .NET zawsze działają co najmniej dwa wątki: wątek główny i wątek wyrzucania śmieci. Linia 1 tworzy nową instancję klasy testowej. W linii 2 tworzymy delegata ThreadStart i przekazujemy adres procedury SubtractFromCounter dla instancji klasy testowej utworzonej w linii 1 (procedura ta jest wywoływana bez parametrów). DobryImportując przestrzeń nazw Threading, można pominąć długą nazwę. Nowy obiekt wątku jest tworzony w wierszu 3. Zwróć uwagę na przekazanie delegata ThreadStart podczas wywoływania konstruktora klasy Thread. Niektórzy programiści wolą łączyć te dwie linie w jedną logiczną linię:

    Dim bThread As New Thread (Nowy ThreadStarttAddressOf _

    mójTest.OdejmijOdLicznika))

    Na koniec wiersz 4 „rozpoczyna” wątek, wywołując metodę Start wystąpienia Thread utworzonego dla delegata ThreadStart. Wywołując tę ​​metodę, informujemy system operacyjny, że procedura Subtract powinna działać w osobnym wątku.

    Słowo „rozpoczyna się” w poprzednim akapicie jest ujęte w cudzysłów, ponieważ jest to jedna z wielu osobliwości programowania wielowątkowego: wywołanie Start tak naprawdę nie uruchamia wątku! Mówi tylko systemowi operacyjnemu, aby zaplanował uruchomienie określonego wątku, ale bezpośrednie uruchomienie programu jest poza kontrolą programu. Nie będziesz w stanie samodzielnie rozpocząć wykonywania wątków, ponieważ system operacyjny zawsze kontroluje wykonywanie wątków. W dalszej części dowiesz się, jak używać priorytetu, aby system operacyjny szybciej uruchamiał wątek.

    Na ryc. 10.1 pokazuje przykład tego, co może się stać po uruchomieniu programu, a następnie przerwaniu go za pomocą klawisza Ctrl + Break. W naszym przypadku nowy wątek zaczął się dopiero po tym, jak licznik w wątku głównym wzrósł do 341!

    Ryż. 10.1. Proste, wielowątkowe środowisko uruchomieniowe

    Jeśli program działa przez dłuższy czas, wynik będzie wyglądał podobnie do pokazanego na rys. 10.2. Widzimy, że tyzakończenie trwającego wątku jest zawieszone, a sterowanie ponownie przeniesione do głównego wątku. W tym przypadku jest manifestacja wielowątkowość z wywłaszczaniem poprzez podział czasu. Znaczenie tego przerażającego terminu wyjaśniono poniżej.

    Ryż. 10.2. Przełączanie się między wątkami w prostym programie wielowątkowym

    Podczas przerywania wątków i przenoszenia kontroli do innych wątków system operacyjny stosuje zasadę wywłaszczania wielowątkowości poprzez podział czasu. Kwantyzacja czasu rozwiązuje również jeden z typowych problemów, które pojawiały się wcześniej w programach wielowątkowych - jeden wątek zajmuje cały czas procesora i nie jest gorszy od kontroli innych wątków (z reguły dzieje się to w intensywnych cyklach, takich jak ten powyżej). Aby zapobiec wyłącznemu przechwyceniu procesora, Twoje wątki powinny od czasu do czasu przekazywać kontrolę innym wątkom. Jeśli program okaże się „nieświadomy”, istnieje inne, nieco mniej pożądane rozwiązanie: system operacyjny zawsze wywłaszcza działający wątek, niezależnie od jego priorytetu, dzięki czemu dostęp do procesora ma każdy wątek w systemie.

    Ponieważ schematy kwantyzacji wszystkich wersji systemu Windows, w których działa platforma .NET, mają minimalny przedział czasu przydzielony do każdego wątku, w programowaniu .NET problemy z przechwytywaniem wyłączności procesora nie są tak poważne. Z drugiej strony, jeśli platforma .NET zostanie kiedykolwiek dostosowana do innych systemów, może się to zmienić.

    Jeśli umieścimy następujący wiersz w naszym programie przed wywołaniem Start, to nawet wątki o najniższym priorytecie otrzymają ułamek czasu procesora:

    bWątek.Priority = Priorytet wątku.Najwyższy

    Ryż. 10.3. Wątek o najwyższym priorytecie zwykle uruchamia się szybciej

    Ryż. 10.4. Procesor jest również przewidziany dla wątków o niższym priorytecie

    Polecenie przypisuje maksymalny priorytet nowemu wątkowi i zmniejsza priorytet głównego wątku. Figa. 10.3 widać, że nowy wątek zaczyna działać szybciej niż wcześniej, ale jak na rys. 10.4, główny wątek również otrzymuje kontrolęlenistwo (choć na bardzo krótki czas i dopiero po dłuższej pracy przepływu z odejmowaniem). Po uruchomieniu programu na swoich komputerach otrzymasz wyniki podobne do tych pokazanych na ryc. 10.3 i 10.4, ale ze względu na różnice między naszymi systemami nie będzie dokładnego dopasowania.

    Typ wyliczeniowy ThreadPrlority zawiera wartości dla pięciu poziomów priorytetów:

    Priorytet wątku.Najwyższy

    ThreadPriority.AboveNormal

    ThreadPrlority.Normal

    ThreadPriority.BelowNormal

    Priorytet wątku. Najniższy

    Dołącz do metody

    Czasami wątek programu musi zostać wstrzymany do czasu zakończenia innego wątku. Załóżmy, że chcesz wstrzymać wątek 1, dopóki wątek 2 nie zakończy swoich obliczeń. Dla tego ze strumienia 1 metoda Join jest wywoływana dla strumienia 2. Innymi słowy, polecenie

    wątek2.Dołącz ()

    zawiesza bieżący wątek i czeka na zakończenie wątku 2. Wątek 1 przechodzi do stan zablokowany.

    Jeśli dołączysz do strumienia 1 do strumienia 2 za pomocą metody Dołącz, system operacyjny automatycznie uruchomi strumień 1 po strumieniu 2. Pamiętaj, że proces uruchamiania jest niedeterministyczny: nie można dokładnie powiedzieć, jak długo po zakończeniu wątku 2 zacznie działać wątek 1. Istnieje inna wersja Join, która zwraca wartość logiczną:

    thread2.Join (liczba całkowita)

    Ta metoda albo czeka na zakończenie wątku 2, albo odblokowuje wątek 1 po upływie określonego czasu, powodując, że harmonogram systemu operacyjnego ponownie przydzieli czas procesora do wątku. Metoda zwraca True, jeśli wątek 2 zakończy się przed upływem określonego limitu czasu, a False w przeciwnym razie.

    Zapamiętaj podstawową zasadę: czy wątek 2 został zakończony, czy upłynął limit czasu, nie masz kontroli nad tym, kiedy wątek 1 jest aktywowany.

    Nazwy wątków, CurrentThread i ThreadState

    Właściwość Thread.CurrentThread zwraca odwołanie do aktualnie wykonywanego obiektu wątku.

    Chociaż istnieje wspaniałe okno wątku do debugowania aplikacji wielowątkowych w VB .NET, które opisano poniżej, bardzo często pomagało nam polecenie

    MsgBox (Wątek.BieżącyWątek.Nazwa)

    Często okazywało się, że kod był wykonywany w zupełnie innym wątku, z którego miał być wykonywany.

    Przypomnijmy, że termin „niedeterministyczne planowanie przepływów programów” oznacza bardzo prostą rzecz: programista praktycznie nie ma do dyspozycji żadnych środków, aby wpłynąć na pracę programu planującego. Z tego powodu programy często używają właściwości ThreadState do zwracania informacji o bieżącym stanie wątku.

    Okno strumieni

    Okno wątków programu Visual Studio .NET jest nieocenione w debugowaniu programów wielowątkowych. Jest aktywowany przez polecenie podmenu Debug> Windows w trybie przerwań. Załóżmy, że przypisałeś nazwę wątkowi bThread za pomocą następującego polecenia:

    bThread.Name = "Odejmowanie wątku"

    Przybliżony widok okna strumieni po przerwaniu programu kombinacją klawiszy Ctrl + Break (lub w inny sposób) pokazano na rys. 10.5.

    Ryż. 10.5. Okno strumieni

    Strzałka w pierwszej kolumnie oznacza aktywny wątek zwrócony przez właściwość Thread.CurrentThread. Kolumna ID zawiera numeryczne identyfikatory wątków. Następna kolumna zawiera nazwy strumieni (jeśli zostały przypisane). Kolumna Location wskazuje procedurę do uruchomienia (na przykład procedura WriteLine klasy Console na rysunku 10.5). Pozostałe kolumny zawierają informacje o wątkach priorytetowych i zawieszonych (patrz następna sekcja).

    Okno wątków (nie system operacyjny!) Umożliwia kontrolowanie wątków programu za pomocą menu kontekstowych. Na przykład, możesz zatrzymać bieżący wątek, klikając prawym przyciskiem myszy odpowiedni wiersz i wybierając polecenie Zamroź (później zatrzymany wątek można wznowić). Zatrzymywanie wątków jest często używane podczas debugowania, aby zapobiec zakłócaniu działania aplikacji przez nieprawidłowo działający wątek. Ponadto okno strumieni umożliwia aktywację innego (niezatrzymanego) strumienia; w tym celu kliknij prawym przyciskiem myszy wymaganą linię i wybierz polecenie Przełącz na wątek z menu kontekstowego (lub po prostu kliknij dwukrotnie linię wątku). Jak zostanie pokazane poniżej, jest to bardzo przydatne w diagnozowaniu potencjalnych zakleszczeń.

    Zawieszanie strumienia

    Tymczasowo nieużywane strumienie można przenieść do stanu pasywnego za pomocą metody Sleer. Strumień pasywny jest również uważany za zablokowany. Oczywiście, gdy wątek zostanie wprowadzony w stan pasywny, pozostałe wątki będą miały więcej zasobów procesora. Standardowa składnia metody Sleer jest następująca: Thread.Sleep (interval_in_milliseconds)

    W wyniku wywołania Sleep aktywny wątek staje się pasywny na co najmniej określoną liczbę milisekund (jednak aktywacja natychmiast po upływie określonego interwału nie jest gwarantowana). Uwaga: podczas wywoływania metody nie jest przekazywane odwołanie do konkretnego wątku - metoda Sleep jest wywoływana tylko dla aktywnego wątku.

    Inna wersja trybu uśpienia powoduje, że bieżący wątek rezygnuje z reszty przydzielonego czasu procesora:

    Wątek.Uśpienie (0)

    Kolejna opcja wprowadza bieżący wątek w stan pasywny na czas nieograniczony (aktywacja następuje tylko w przypadku wywołania przerwania):

    Thread.Sleer (Timeout.Infinite)

    Ponieważ wątki pasywne (nawet z nieograniczonym limitem czasu) mogą zostać przerwane przez metodę Interrupt, która prowadzi do zainicjowania wyjątku ThreadlnterruptExcepti, wywołanie Slayera jest zawsze zawarte w bloku Try-Catch, jak w poniższym fragmencie:

    Próbować

    Wątek.Sen (200)

    „Pasywny stan wątku został przerwany

    Złap e jako wyjątek

    „Inne wyjątki

    Koniec prób

    Każdy program .NET działa w wątku programu, więc metoda Sleep służy również do zawieszania programów (jeśli przestrzeń nazw Threadipg nie jest importowana przez program, należy użyć w pełni kwalifikowanej nazwy Threading.Thread. Sleep).

    Zakończenie lub przerwanie wątków programu

    Wątek zostanie automatycznie zakończony, gdy metoda określona podczas tworzenia delegata ThreadStart, ale czasami metoda (a tym samym wątek) będzie musiała zakończyć się, gdy wystąpią pewne czynniki. W takich przypadkach strumienie zwykle sprawdzają zmienna warunkowa, w zależności od stanu któregopodejmowana jest decyzja o ewakuacji ze strumienia. Zazwyczaj w procedurze zawarta jest pętla Do-While:

    Podrzędna metoda wątków ()

    „Program musi zapewnić środki na przeprowadzenie ankiety

    „zmienna warunkowa.

    „Na przykład zmienna warunkowa może być stylizowana jako właściwość

    Zrób Chociaż warunek Zmienna = False I WięcejPracyDo Zrobienia

    „Główny kod

    Koniec pętli Sub

    Odpytanie zmiennej warunkowej zajmuje trochę czasu. Należy używać trwałego sondowania w warunkach pętli tylko wtedy, gdy czekasz na przedwczesne zakończenie wątku.

    Jeśli zmienna warunku musi być sprawdzona w określonej lokalizacji, użyj polecenia If-Then w połączeniu z Exit Sub wewnątrz nieskończonej pętli.

    Dostęp do zmiennej warunkowej musi być zsynchronizowany, aby ekspozycja z innych wątków nie zakłócała ​​jej normalnego użytkowania. Ten ważny temat został omówiony w sekcji „Rozwiązywanie problemów: synchronizacja”.

    Niestety kod wątków pasywnych (lub w inny sposób zablokowanych) nie jest wykonywany, więc opcja z odpytywaniem zmiennej warunkowej nie jest dla nich odpowiednia. W takim przypadku wywołaj metodę Interrupt na zmiennej obiektu, która zawiera odwołanie do żądanego wątku.

    Metodę Interrupt można wywoływać tylko w wątkach w stanie Wait, Sleep lub Join. Jeśli wywołasz Interrupt dla wątku, który jest w jednym z wymienionych stanów, po chwili wątek zacznie ponownie działać, a środowisko wykonawcze zainicjuje wyjątek ThreadlnterruptedExcepti w wątku. Dzieje się tak, nawet jeśli wątek stał się pasywny na czas nieokreślony przez wywołanie Thread.Sleepdimeout. Nieskończony). Mówimy „po chwili”, ponieważ planowanie wątków nie jest deterministyczne. Wyjątek ThreadlnterruptedExcepti on jest przechwytywany przez sekcję Catch zawierającą kod wyjścia ze stanu oczekiwania. Jednak sekcja Catch nie musi kończyć wątku w wywołaniu przerwania — wątek obsługuje wyjątek według własnego uznania.

    W .NET metodę Interrupt można wywołać nawet w przypadku wątków niezablokowanych. W takim przypadku wątek zostaje przerwany przy najbliższym zablokowaniu.

    Zawieszanie i zabijanie wątków

    Przestrzeń nazw Threading zawiera inne metody, które przerywają normalne wątki:

    • Wstrzymać;
    • Anulować.

    Trudno powiedzieć, dlaczego platforma .NET zawierała obsługę tych metod - gdy wywołasz Suspend i Abort, program najprawdopodobniej stanie się niestabilny. Żadna z metod nie pozwala na normalną deinicjalizację strumienia. Ponadto, gdy wywołujesz Suspend lub Abort, nie możesz przewidzieć, w jakim stanie wątek pozostawi obiekty po zawieszeniu lub przerwaniu.

    Wywołanie Abort zgłasza ThreadAbortException. Aby pomóc Ci zrozumieć, dlaczego ten dziwny wyjątek nie powinien być obsługiwany w programach, oto fragment dokumentacji .NET SDK:

    „... Gdy wątek zostanie zniszczony przez wywołanie Abort, środowisko uruchomieniowe zgłasza ThreadAbortException. Jest to specjalny rodzaj wyjątku, którego program nie może przechwycić. Po zgłoszeniu tego wyjątku środowisko uruchomieniowe uruchamia wszystkie bloki Last przed zakończeniem wątku. Ponieważ każda akcja może mieć miejsce w blokach Final, wywołaj Join, aby upewnić się, że strumień zostanie zniszczony”.

    Morał: przerwanie i wstrzymanie nie są zalecane (a jeśli nadal nie możesz obejść się bez wstrzymania, wznów zawieszony wątek za pomocą metody Wznów). Możesz bezpiecznie zakończyć wątek tylko przez odpytywanie zsynchronizowanej zmiennej warunku lub przez wywołanie metody Interrupt omówionej powyżej.

    Wątki tła (demonów)

    Niektóre wątki działające w tle automatycznie przestają działać po zatrzymaniu innych składników programu. W szczególności garbage collector działa w jednym z wątków w tle. Wątki w tle są zwykle tworzone w celu odbierania danych, ale odbywa się to tylko wtedy, gdy inne wątki uruchamiają kod, który może przetwarzać odebrane dane. Składnia: nazwa strumienia IsBackGround = True

    Jeśli w aplikacji pozostaną tylko wątki w tle, aplikacja zostanie automatycznie zamknięta.

    Poważniejszy przykład: wyodrębnianie danych z kodu HTML

    Zalecamy korzystanie ze strumieni tylko wtedy, gdy funkcjonalność programu jest wyraźnie podzielona na kilka operacji. Dobrym przykładem jest program do wyodrębniania HTML z rozdziału 9. Nasza klasa robi dwie rzeczy: pobiera dane z Amazona i przetwarza je. To doskonały przykład sytuacji, w której programowanie wielowątkowe jest naprawdę odpowiednie. Tworzymy klasy dla kilku różnych książek, a następnie analizujemy dane w różnych strumieniach. Tworzenie nowego wątku dla każdej książki zwiększa wydajność programu, ponieważ podczas gdy jeden wątek odbiera dane (co może wymagać czekania na serwerze Amazon), inny wątek będzie zajęty przetwarzaniem danych, które już zostały odebrane.

    Wersja wielowątkowa tego programu działa wydajniej niż wersja jednowątkowa tylko na komputerze z kilkoma procesorami lub jeśli odbiór dodatkowych danych można skutecznie połączyć z ich analizą.

    Jak wspomniano powyżej, tylko procedury, które nie mają parametrów, mogą być uruchamiane w wątkach, więc będziesz musiał dokonać drobnych zmian w programie. Poniżej znajduje się podstawowa procedura, przepisana w celu wykluczenia parametrów:

    Publiczna pozycja podrzędna FindRank ()

    m_Rank = ZdrapAmazonka ()

    Console.WriteLine ("ranga" & m_Name & "Jest" & GetRank)

    Napis końcowy

    Ponieważ nie będziemy mogli użyć połączonego pola do przechowywania i pobierania informacji (pisanie programów wielowątkowych z interfejsem graficznym jest omówione w ostatniej części tego rozdziału), program przechowuje dane czterech książek w tablicy, definicja, która zaczyna się tak:

    Dim theBook (3.1) As String theBook (0.0) = "1893115992"

    theBook (0.l) = "Programowanie VB .NET" "Id.

    W tym samym cyklu, w którym tworzone są obiekty AmazonRanker, tworzone są cztery strumienie:

    Dla i = 0 do 3

    Próbować

    theRanker = Nowy AmazonRanker (theBook (i.0). theBookd.1))

    aThreadStart = New ThreadStar (AddressOf theRanker.FindRan ()

    aThread = Nowy wątek (aThreadStart)

    aWątek.Nazwa = Księga (i.l)

    aWątek.Start () Złap jako wyjątek

    Console.WriteLine (e.Message)

    Koniec prób

    Następny

    Poniżej pełny tekst programu:

    Opcja ścisła przy imporcie System.IO Importuje System.Net

    Importuje system. Threading

    Moduł Modułl

    Sub Główny ()

    Przyciemnij książkę (3.1) jako ciąg

    książka (0.0) = "1893115992"

    theBook (0.l) = "Programowanie VB .NET"

    książka (l.0) = "1893115291"

    theBook (l.l) = "Programowanie baz danych VB .NET"

    książka (2,0) = "1893115623"

    theBook (2.1) = „Wprowadzenie programisty do C#”.

    książka (3.0) = "1893115593"

    theBook (3.1) = "Gland the .Net Platform"

    Dim i jako liczba całkowita

    Dim theRanker As = AmazonRanker

    Dim aThreadStart jako Threading.ThreadStart

    Przyciemnij wątek jako wątek

    Dla i = 0 do 3

    Próbować

    theRanker = New AmazonRankerttheBook (i.0). Księga (i.1))

    aThreadStart = New ThreadStart (AdresOf theRanker. FindRank)

    aThread = Nowy wątek (aThreadStart)

    aWątek.Nazwa = Księga (i.l)

    aWątek.Start ()

    Złap e jako wyjątek

    Konsola.WriteLlnete.Wiadomość)

    Koniec Spróbuj Dalej

    Konsola.ReadLine()

    Napis końcowy

    Moduł końcowy

    Klasa publiczna AmazonRanker

    Prywatny m_URL jako ciąg

    Prywatny m_Rank As Integer

    Prywatne m_Name jako ciąg

    Public Sub New (ByVal ISBN As String. ByVal theName As String)

    m_URL = "http://www.amazon.com/exec/obidos/ASIN/" & ISBN

    m_Name = nazwa Koniec Sub

    Publiczna Sub Ranga FindRank () m_Rank = ScrapeAmazon ()

    Console.Writeline ("ranga" & m_Name & "jest"

    & GetRank) Koniec napisu

    Właściwość publiczna tylko do odczytu GetRank () As String Get

    Jeśli m_Rank<>0 Wtedy

    Zwróć CStr (m_Rank) Inny

    Problemy

    Zakończ, jeśli

    Koniec Pobierz

    Koniec właściwości

    Właściwość publiczna tylko do odczytu GetName () As String Get

    Zwróć m_Imię

    Koniec Pobierz

    Koniec właściwości

    Funkcja prywatna ScrapeAmazon () jako liczba całkowita Try

    Przyciemnij adres URL jako nowy identyfikator Uri (m_URL)

    Przyciemnij żądanie jako żądanie sieciowe

    theRequest = WebRequest.Create (URL)

    Przyciemnij odpowiedź jako odpowiedź internetową

    theResponse = theRequest.GetResponse

    Dim aReader jako nowy StreamReader (theResponse.GetResponseStream ())

    Przyciemnij dane jako ciąg

    theData = aReader.ReadToEnd

    Analiza zwrotu (theData)

    Złap E jako wyjątek

    Console.WriteLine (Wiadomość E)

    Console.WriteLine (E.StackTrace)

    Konsola. Czytaj linię ()

    Zakończ Spróbuj Zakończ funkcję

    Analiza funkcji prywatnych (ByVal theData As String) jako liczba całkowita

    Położenie Dim As.Integer Położenie = theData.IndexOf (" Amazon.com

    Ranking sprzedaży:") _

    + "Ranking sprzedaży Amazon.com:".Długość

    Dim temp As String

    Dopóki theData.Substring (Location.l) = "<" temp = temp

    & theData.Substring (Location.l) Lokalizacja + = 1 pętla

    Powrót Clnt (temp)

    Koniec funkcji

    Koniec klasy

    Operacje wielowątkowe są powszechnie używane w przestrzeniach nazw .NET i I/O, dlatego biblioteka .NET Framework udostępnia dla nich specjalne metody asynchroniczne. Aby uzyskać więcej informacji na temat używania metod asynchronicznych podczas pisania programów wielowątkowych, zobacz metody BeginGetResponse i EndGetResponse klasy HTTPWebRequest.

    Główne zagrożenie (dane ogólne)

    Do tej pory rozważano jedyny bezpieczny przypadek użycia wątków - nasze streamy nie zmieniły ogólnych danych. Jeśli pozwolisz na zmianę ogólnych danych, potencjalne błędy zaczynają się mnożyć wykładniczo i znacznie trudniej jest się ich pozbyć dla programu. Z drugiej strony, jeśli zabronisz modyfikowania współdzielonych danych przez różne wątki, wielowątkowe programowanie .NET prawie nie będzie się różnić od ograniczonych możliwości VB6.

    Chcielibyśmy zwrócić Państwa uwagę na mały program, który pokazuje pojawiające się problemy bez wchodzenia w niepotrzebne szczegóły. Ten program symuluje dom z termostatem w każdym pomieszczeniu. Jeśli temperatura jest o 5 stopni Fahrenheita lub więcej (około 2,77 stopni Celsjusza) niższa od docelowej, zlecamy systemowi grzewczemu podwyższenie temperatury o 5 stopni; w przeciwnym razie temperatura wzrośnie tylko o 1 stopień. Jeśli aktualna temperatura jest większa lub równa ustawionej, zmiana nie jest dokonywana. Regulacja temperatury w każdym pomieszczeniu odbywa się oddzielnym przepływem z 200-milisekundowym opóźnieniem. Główna praca jest wykonywana za pomocą następującego fragmentu:

    Jeśli mHouse.HouseTemp< mHouse.MAX_TEMP = 5 Then Try

    Wątek.Sen (200)

    Złap remis jako ThreadlnterruptedException

    „Oczekiwanie bierne zostało przerwane

    Złap e jako wyjątek

    „Inne wyjątki dotyczące prób zakończenia

    mHouse.HouseTemp + - 5 "itd.

    Poniżej znajduje się pełny kod źródłowy programu. Wynik pokazano na ryc. 10,6: Temperatura w domu osiągnęła 105 stopni Fahrenheita (40,5 stopni Celsjusza)!

    1 opcja ściśle włączona

    2 Importuje system. Wątek

    Moduł 3 modułów

    4 Sub Główny ()

    5 Dim myHouse jako nowy dom (l0)

    6 Konsola. Czytaj linię ()

    7 Koniec Sub

    8 Moduł końcowy

    9 Dom klasy publicznej

    10 Public Const MAX_TEMP jako liczba całkowita = 75

    11 Prywatny mCurTemp jako liczba całkowita = 55

    12 pokoi prywatnych () jako pokój

    13 Subskrypcja publiczna Nowa (ByVal numOfRooms jako liczba całkowita)

    14 ReDim mRooms (liczbaRooms = 1)

    15 Dim i jako liczba całkowita

    16 Dim aThreadStart jako Threading.ThreadStart

    17 Przyciemnij wątek jako wątek

    18 Dla i = 0 To numOfRooms -1

    19 Spróbuj

    20 mPokoje (i) = NowyPokój (Ja, mCurTemp, CStr (i) i „pokój”)

    21 aRozpoczęcie wątku — nowy początek wątku (adres _

    mPokoje (i) .SprawdźTempWPok)

    22 aThread = Nowy wątek (aThreadStart)

    23 aWątek.Start ()

    24 Złap E jako wyjątek

    25 Konsola.WriteLine (E.StackTrace)

    26 Koniec prób

    27 Dalej

    28 Koniec Sub

    29 Własność publiczna HouseTemp () jako liczba całkowita

    trzydzieści . Dostwać

    31 Powrót mCurTemp

    32 Koniec Pobierz

    33 Ustaw (wartość ByVal jako liczba całkowita)

    34 mCurTemp = Wartość 35 Koniec Set

    36 Koniec własności

    37 Koniec klasy

    38 Publiczna sala lekcyjna

    39 Prywatny mCurTemp jako liczba całkowita

    40 Prywatne mName jako ciąg

    41 Prywatny mHouse As House

    42 Public Sub New (ByVal theHouse As House,

    ByVal temp As Integer, ByVal roomName As String)

    43 mDom = dom

    44 mCurTemp = temp

    45 mNazwa = NazwaPomieszczenia

    46 Koniec Sub

    47 Public Sub CheckTempInRoom ()

    48 Zmiana temperatury ()

    49 Koniec Sub

    50 Prywatne Sub ChangeTemperature ()

    51 Spróbuj

    52 Jeśli mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then

    53 Wątek.Sen (200)

    54 mHouse.HouseTemp + - 5

    55 Console.WriteLine ("Jestem w" & Me.mName & _

    56 „.Aktualna temperatura to” i mHouse.HouseTemp)

    57. Elself mHouse.HouseTemp< mHouse.MAX_TEMP Then

    58 Wątek.Sen (200)

    59 mHouse.HouseTemp + = 1

    60 Console.WriteLine ("Jestem w" & Me.mName & _

    61 „.Aktualna temperatura to” i mHouse.HouseTemp)

    62 Jeszcze

    63 Console.WriteLine ("Jestem w" & Me.mName & _

    64 „.Aktualna temperatura to” i mHouse.HouseTemp)

    65 „Nic nie rób, temperatura jest w normie

    66 Koniec jeśli

    67 Złap jako ThreadlnterruptedException

    68 „Oczekiwanie pasywne zostało przerwane

    69 Złap jako wyjątek

    70 „Inne wyjątki

    71 Koniec prób

    72 Koniec Sub

    73 Koniec klasy

    Ryż. 10.6. Problemy z wielowątkowością

    Procedura Sub Main (wiersze 4-7) tworzy „dom” z dziesięcioma „pokojami”. Klasa House ustala maksymalną temperaturę 75 stopni Fahrenheita (około 24 stopni Celsjusza). Linie 13-28 definiują dość złożonego konstruktora domu. Kluczem do zrozumienia programu są wiersze 18-27. Linia 20 tworzy kolejny obiekt pokoju, a odniesienie do obiektu domu jest przekazywane do konstruktora, aby obiekt pokoju mógł się do niego odwoływać w razie potrzeby. Linie 21-23 uruchamiają dziesięć strumieni, aby dostosować temperaturę w każdym pomieszczeniu. Klasa Room jest zdefiniowana w wierszach 38-73. Odniesienie do domu coxpajest przechowywany w zmiennej mHouse w konstruktorze klasy Room (wiersz 43). Kod do sprawdzania i regulacji temperatury (linie 50-66) wygląda prosto i naturalnie, ale jak się wkrótce przekonasz, to wrażenie myli! Zauważ, że ten kod jest opakowany w blok Try-Catch, ponieważ program używa metody Sleep.

    Mało kto zgodziłby się na życie w temperaturze 105 stopni Fahrenheita (40,5 do 24 stopni Celsjusza). Co się stało? Problem dotyczy następującej linii:

    Jeśli mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then

    I dzieje się tak: najpierw temperatura jest sprawdzana przez przepływ 1. Widzi, że temperatura jest za niska i podnosi ją o 5 stopni. Niestety, zanim temperatura wzrośnie, strumień 1 zostaje przerwany i sterowanie zostaje przekazane do strumienia 2. Strumień 2 sprawdza tę samą zmienną, która nie został jeszcze zmieniony przepływ 1. Zatem przepływ 2 również przygotowuje się do podniesienia temperatury o 5 stopni, ale nie ma na to czasu i również przechodzi w stan oczekiwania. Proces trwa aż do uruchomienia strumienia 1 i przechodzi do kolejnego polecenia - podwyższenia temperatury o 5 stopni. Wzrost powtarza się, gdy wszystkie 10 strumieni zostanie aktywowanych, a mieszkańcy domu będą mieli zły czas.

    Rozwiązanie problemu: synchronizacja

    W poprzednim programie dochodzi do sytuacji, w której wynik programu zależy od kolejności wykonywania wątków. Aby się go pozbyć, musisz upewnić się, że polecenia takie jak

    Jeśli mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then...

    są w pełni przetwarzane przez aktywny wątek, zanim zostanie przerwany. Ta właściwość nazywa się wstyd atomowy - blok kodu musi być wykonywany przez każdy wątek bez przerwy, jako jednostka niepodzielna. Grupa poleceń, połączona w atomowy blok, nie może zostać przerwana przez program planujący wątki, dopóki nie zostanie ukończony. Każdy wielowątkowy język programowania ma swoje własne sposoby na zapewnienie niepodzielności. W VB .NET najłatwiejszym sposobem użycia polecenia SyncLock jest przekazanie zmiennej obiektu po wywołaniu. Wprowadź niewielkie zmiany w procedurze ChangeTemperature z poprzedniego przykładu, a program będzie działał poprawnie:

    Prywatna Sub ChangeTemperature () SyncLock (mHouse)

    Próbować

    Jeśli mHouse.HouseTemp< mHouse.MAXJTEMP -5 Then

    Wątek.Sen (200)

    mHouse.HouseTemp + = 5

    Console.WriteLine ("Jestem w" & Me.mName & _

    „.Aktualna temperatura to” i mHouse.HouseTemp)

    Ja sam

    mHouse.HouseTemp< mHouse. MAX_TEMP Then

    Wątek.Sen (200) mHouse.HouseTemp + = 1

    Console.WriteLine ("Jestem w" & Me.mName & _ ".Aktualna temperatura jest" & mHouse.HomeTemp) Else

    Konsola.WriteLineC "Jestem w" & Me.mNazwa & _ ".Aktualna temperatura to" & mHouse.HouseTemp)

    „Nic nie rób, temperatura jest normalna

    Zakończ, jeśli złap remis jako ThreadlnterruptedException

    „Oczekiwanie pasywne zostało przerwane przez funkcję Catch e As Exception

    „Inne wyjątki

    Koniec prób

    Zakończ synchronizację

    Napis końcowy

    Kod blokowy SyncLock jest wykonywany niepodzielnie. Dostęp do niego ze wszystkich innych wątków zostanie zamknięty, dopóki pierwszy wątek nie zwolni blokady za pomocą polecenia End SyncLock. Jeśli wątek w zsynchronizowanym bloku przechodzi w stan pasywnego oczekiwania, blokada pozostaje do momentu przerwania lub wznowienia wątku.

    Prawidłowe użycie polecenia SyncLock zapewnia bezpieczeństwo wątku programu. Niestety nadużywanie SyncLock ma negatywny wpływ na wydajność. Synchronizacja kodu w programie wielowątkowym kilkukrotnie zmniejsza szybkość jego pracy. Synchronizuj tylko najbardziej potrzebny kod i jak najszybciej zwolnij blokadę.

    Podstawowe klasy kolekcji są niebezpieczne w aplikacjach wielowątkowych, ale .NET Framework zawiera bezpieczne wątkowo wersje większości klas kolekcji. W tych klasach kod potencjalnie niebezpiecznych metod jest zawarty w blokach SyncLock. Bezpieczne wątkowo wersje klas kolekcji powinny być używane w programach wielowątkowych wszędzie tam, gdzie naruszona jest integralność danych.

    Pozostaje wspomnieć, że zmienne warunkowe można łatwo zaimplementować za pomocą polecenia SyncLock. Aby to zrobić, wystarczy zsynchronizować zapis do wspólnej właściwości logicznej, dostępnej do odczytu i zapisu, tak jak w poniższym fragmencie:

    Warunek klasy publicznej Zmienna

    Prywatna udostępniona szafka jako obiekt = nowy obiekt ()

    Private Shared mOK As Boolean Shared

    Właściwość TheCondition Variable () As Boolean

    Dostwać

    Zwrot mOK

    Koniec Pobierz

    Ustaw (ByVal Value As Boolean) SyncLock (szafka)

    mOK = wartość

    Zakończ synchronizację

    Koniec zestawu

    Koniec właściwości

    Koniec klasy

    SyncLock Command and Monitor Class

    Użycie polecenia SyncLock obejmuje pewne subtelności, których nie pokazano w prostych przykładach powyżej. Tak więc wybór obiektu synchronizacji odgrywa bardzo ważną rolę. Spróbuj uruchomić poprzedni program za pomocą polecenia SyncLock (Me) zamiast SyncLock (mHouse). Temperatura ponownie wzrasta powyżej progu!

    Pamiętaj, że polecenie SyncLock synchronizuje się za pomocą obiekt, przekazany jako parametr, a nie przez fragment kodu. Parametr SyncLock działa jako drzwi umożliwiające dostęp do zsynchronizowanego fragmentu z innych wątków. Polecenie SyncLock (Ja) faktycznie otwiera kilka różnych „drzwi”, co jest dokładnie tym, czego próbowałeś uniknąć podczas synchronizacji. Moralność:

    Aby chronić współdzielone dane w aplikacji wielowątkowej, polecenie SyncLock musi synchronizować jeden obiekt na raz.

    Ponieważ synchronizacja jest powiązana z konkretnym obiektem, w niektórych sytuacjach możliwe jest nieumyślne zablokowanie innych fragmentów. Załóżmy, że masz dwie zsynchronizowane metody, pierwszą i drugą, a obie metody są synchronizowane w obiekcie bigLock. Gdy wątek 1 najpierw wprowadzi metodę i przechwyci bigLock, żaden wątek nie będzie mógł wprowadzić drugiej metody, ponieważ dostęp do niej jest już ograniczony do wątku 1!

    Funkcjonalność polecenia SyncLock można traktować jako podzbiór funkcji klasy Monitor. Klasa Monitor jest wysoce konfigurowalna i może być używana do rozwiązywania nietrywialnych zadań synchronizacji. Polecenie SyncLock jest przybliżonym odpowiednikiem metod Enter i Exi t klasy Monitor:

    Próbować

    Monitor.Enter (theObject) Wreszcie

    Monitor.Exit (obiekt)

    Koniec prób

    Dla niektórych standardowych operacji (zwiększanie/zmniejszanie zmiennej, zamiana zawartości dwóch zmiennych) .NET Framework udostępnia klasę Interlocked, której metody wykonują te operacje na poziomie atomowym. Korzystając z klasy Interlocked, te operacje są znacznie szybsze niż przy użyciu polecenia SyncLock.

    Blokowanie

    Podczas synchronizacji blokada jest ustawiana na obiektach, a nie na wątkach, więc podczas używania różne obiekty do zablokowania różne fragmenty kodu w programach czasami pojawiają się dość nietrywialne błędy. Niestety w wielu przypadkach synchronizacja na pojedynczym obiekcie jest po prostu niedopuszczalna, ponieważ prowadzi do zbyt częstego blokowania wątków.

    Rozważ sytuację blokujące(impas) w najprostszej formie. Wyobraź sobie dwóch programistów przy stole. Niestety mają tylko jeden nóż i jeden widelec na dwoje. Zakładając, że do jedzenia potrzebujesz zarówno noża, jak i widelca, możliwe są dwie sytuacje:

    • Jednemu programiście udaje się chwycić za nóż i widelec i zaczyna jeść. Kiedy jest pełny, odkłada obiad na bok, a inny programista może je zabrać.
    • Jeden programista bierze nóż, a drugi widelec. Żaden z nich nie może zacząć jeść, dopóki drugi nie zrezygnuje ze swojego urządzenia.

    W programie wielowątkowym sytuacja ta nazywa się wzajemne blokowanie. Te dwie metody są synchronizowane na różnych obiektach. Wątek A przechwytuje obiekt 1 i wchodzi do części programu chronionej przez ten obiekt. Niestety, aby to zadziałało, potrzebuje dostępu do kodu chronionego inną blokadą synchronizacji z innym obiektem synchronizacji. Ale zanim zdąży wejść do fragmentu, który jest zsynchronizowany przez inny obiekt, strumień B wchodzi do niego i przechwytuje ten obiekt. Teraz wątek A nie może wejść do drugiego fragmentu, wątek B nie może wejść do pierwszego fragmentu, a oba wątki są skazane na czekanie w nieskończoność. Żaden wątek nie może nadal działać, ponieważ wymagany obiekt nigdy nie zostanie zwolniony.

    Diagnozę zakleszczeń komplikuje fakt, że mogą one wystąpić w stosunkowo rzadkich przypadkach. Wszystko zależy od kolejności, w jakiej planista przydziela im czas procesora. Możliwe, że w większości przypadków obiekty synchronizacji zostaną przechwycone w kolejności bez zakleszczenia.

    Poniżej znajduje się implementacja opisanej właśnie sytuacji impasu. Po krótkim omówieniu najbardziej podstawowych punktów pokażemy, jak zidentyfikować sytuację impasu w oknie wątku:

    1 opcja ściśle włączona

    2 Importuje system. Wątek

    Moduł 3 modułów

    4 Sub Główny ()

    5 Dim Tom jako nowy programista („Tom”)

    6 Dim Bob jako nowy programista („Bob”)

    7 Dim aThreadStart jako nowy ThreadStart (AddressOf Tom.Eat)

    8 Przyciemnij aThread jako nowy wątek (aThreadStart)

    9 aThread.Name = "Tomek"

    10 Dim bThreadStart jako nowy ThreadStarttAddressOf Bob.Eat)

    11 Przyciemnij bThread jako nowy wątek (bThreadStart)

    12 bThread.Name = "Bob"

    13 aWątek.Start ()

    14 bWątek.Start ()

    15 Koniec Sub

    16 Moduł końcowy

    17 Widelec klasy publicznej

    18 Private Shared mForkAvaiTable As Boolean = True

    19 Prywatny współużytkowany kosiarka As String = „Nikt”

    20 Własność prywatna tylko do odczytu OwnsUtensil () As String

    21 Pobierz

    22 Powrót kosiarki

    23 Koniec Pobierz

    24 Koniec nieruchomości

    25 Public Sub GrabForktByVal jako programista)

    26 Konsola.Writel_ine (Wątek.BieżącyWątek.Nazwa i _

    "próbuję złapać widelec")

    27 Console.WriteLine (Me.OwnsUtensil i "ma widelec."). ...

    28 Monitor.Enter (Me) "SyncLock (aFork)"

    29 Jeśli mFork jest dostępny, to

    30 a.HasFork = Prawda

    31 Właściciel = a.MyName

    32 mWidełDostępny = Fałszywe

    33 Console.WriteLine (a.MyName i „właśnie mam rozwidlenie.czeka”)

    34 Spróbuj

    Thread.Sleep (100) Złap jako wyjątek Console.WriteLine (e.StackTrace)

    Koniec prób

    35 Koniec jeśli

    36 Wyjście monitora (ja)

    Zakończ synchronizację

    37 Koniec Sub

    38 Koniec klasy

    39 Nóż klasy publicznej

    40 Private Shared mKnifeDostępne jako Boolean = Prawda

    41 Prywatny współużytkowany kosiarka As String = „Nikt”

    42 Prywatna właściwość tylko do odczytu OwnsUtensi1 () As String

    43 Pobierz

    44 Powrót kosiarki

    45 Koniec Pobierz

    46 Koniec własności

    47 Public Sub GrabKnifetByVal jako programista)

    48 Konsola.WriteLine (Wątek.BieżącyWątek.Nazwa & _

    "próbuje złapać nóż.")

    49 Console.WriteLine (Me.OwnsUtensil & "ma nóż.")

    50 Monitor.Enter (ja) „SyncLock (aKnife)”

    51 Jeśli mKnife jest dostępny, to

    52 mNóżDostępne = Fałsz

    53 a.HasNóż = Prawda

    54 Właściciel = a.MyName

    55 Console.WriteLine (a.MyName & "właśnie dostałem nóż.czekam")

    56 Spróbuj

    Wątek.Uśpienie (100)

    Złap e jako wyjątek

    Console.WriteLine (e.StackTrace)

    Koniec prób

    57 Koniec jeśli

    58 Wyjście monitora (ja)

    59 Koniec Sub

    60 Koniec klasy

    61 Programista klas publicznych

    62 Prywatne mName jako ciąg

    63 Prywatne Udostępnione mFork As Fork

    64 Prywatny udostępniony mKnife As Knife

    65 Prywatny mHasKnife As Boolean

    66 Prywatny mHasFork jako Boolean

    67 Shared Sub Nowy ()

    68 mWidelec = Nowy widelec ()

    69 mNóż = Nowy Nóż ()

    70 Koniec Sub

    71 Public Sub New (ByVal theName As String)

    72 mImię = Imię

    73 Koniec Sub

    74 Właściwość publiczna tylko do odczytu MyName () As String

    75 Pobierz

    76 Powrót mImię

    77 Koniec Pobierz

    78 Koniec własności

    79 Własność publiczna HasKnife () As Boolean

    80 Pobierz

    81 Powrót mHasNóż

    82 Koniec Pobierz

    83 Ustaw (wartość ByVal jako Boolean)

    84 mHasNóż = Wartość

    85 Koniec zestawu

    86 Koniec nieruchomości

    87 HasFork własności publicznej () jako Boolean

    88 Pobierz

    89 Powrót mHasFork

    90 Koniec Pobierz

    91 Ustaw (wartość ByVal jako Boolean)

    92 mHasFork = Wartość

    93 Koniec zestawu

    94 Koniec własności

    95 Publiczny Posiłek ()

    96 Rób, aż ja będę miał nóż i ja mam widelec

    97 Console.Writeline (Thread.CurrentThread.Name i „jest w wątku.”)

    98 Jeśli Rnd ()< 0.5 Then

    99 mWidelec.ChwytWidelec (Ja)

    100 innych

    101 mNóż.GrabNóż (ja)

    102 Koniec jeśli

    Pętla 103

    104 MsgBox (Me.MyName i „mogę jeść!”)

    105 mNóż = Nowy Nóż ()

    106 mWidelec = Nowy widelec ()

    107 Koniec Sub

    108 Koniec klasy

    Główna procedura Main (linie 4-16) tworzy dwie instancje klasy Programmer, a następnie uruchamia dwa wątki w celu wykonania krytycznej metody Eat klasy Programmer (linie 95-108), opisanej poniżej. Procedura Main ustawia nazwy wątków i konfiguruje je; chyba wszystko, co się dzieje, jest zrozumiałe i bez komentarza.

    Bardziej interesujący jest kod klasy Fork (linie 17-38) (podobna klasa Knife jest zdefiniowana w liniach 39-60). Linie 18 i 19 określają wartości wspólnych pól, dzięki którym można dowiedzieć się, czy wtyczka jest aktualnie dostępna, a jeśli nie, to kto z niej korzysta. Właściwość ReadOnly OwnUtensi1 (wiersze 20-24) jest przeznaczona do najprostszego przekazywania informacji. Centralnym elementem klasy Fork jest metoda GrabFork „chwyć widelec”, zdefiniowana w wierszach 25-27.

    1. Wiersze 26 i 27 po prostu wyświetlają informacje debugowania na konsoli. W głównym kodzie metody (linie 28-36) dostęp do widełek jest synchronizowany przez obiektpas Mnie. Ponieważ nasz program używa tylko jednego rozwidlenia, synchronizacja Ja zapewnia, że ​​żadne dwa wątki nie będą mogły go pobrać w tym samym czasie. Polecenie „p Slee” (w bloku zaczynającym się w wierszu 34) symuluje opóźnienie między chwyceniem widelca/noża a rozpoczęciem jedzenia. Zwróć uwagę, że polecenie Sleep nie odblokowuje obiektów, a jedynie przyspiesza zakleszczenia!
      Najciekawszy jest jednak kod klasy Programmer (linie 61-108). Wiersze 67-70 definiują ogólny konstruktor, aby zapewnić, że w programie jest tylko jeden widelec i nóż. Kod właściwości (linie 74-94) jest prosty i nie wymaga komentarza. Najważniejsza rzecz dzieje się w metodzie Eat, która realizowana jest przez dwa oddzielne wątki. Proces trwa w pętli, aż jakiś strumień przechwyci widelec wraz z nożem. W liniach 98-102 obiekt losowo chwyta widelec / nóż za pomocą wywołania Rnd, co powoduje zakleszczenie. Dzieje się tak:
      Wątek, który wykonuje metodę Eat Tot, jest wywoływany i wchodzi do pętli. Chwyta nóż i przechodzi w stan oczekiwania.
    2. Wątek wykonujący metodę Boba Eat jest wywoływany i wchodzi w pętlę. Nie może chwycić noża, ale chwyta widelec i przechodzi w stan gotowości.
    3. Wątek, który wykonuje metodę Eat Tot, jest wywoływany i wchodzi do pętli. Próbuje chwycić widelec, ale Bob już chwycił widelec; wątek przechodzi w stan oczekiwania.
    4. Wątek wykonujący metodę Boba Eat jest wywoływany i wchodzi w pętlę. Próbuje chwycić nóż, ale nóż jest już schwytany przez obiekt Thoth; wątek przechodzi w stan oczekiwania.

    Wszystko to trwa w nieskończoność - mamy do czynienia z typową sytuacją impasu (spróbuj uruchomić program, a zobaczysz, że nikt nie jest w stanie jeść w ten sposób).
    Możesz również sprawdzić, czy wystąpiło zakleszczenie w oknie wątków. Uruchom program i przerwij go klawiszami Ctrl + Break. Dołącz zmienną Me do rzutni i otwórz okno strumieni. Wynik wygląda podobnie do pokazanego na ryc. 10.7. Z rysunku widać, że nić Boba chwyciła nóż, ale nie ma widelca. Kliknij prawym przyciskiem myszy w oknie Wątki w wierszu Suma i wybierz polecenie Przełącz na wątek z menu kontekstowego. Widok pokazuje, że strumień Thotha ma widelec, ale nie ma noża. Oczywiście nie jest to stuprocentowy dowód, ale takie zachowanie przynajmniej każe podejrzewać, że coś było nie tak.
    Jeśli opcja z synchronizacją o jeden obiekt (jak w programie ze zwiększaniem temperatury w domu) nie jest możliwa, aby zapobiec wzajemnym blokadom, można ponumerować obiekty synchronizacji i zawsze przechwytywać je w stałej kolejności. Kontynuujmy analogię z programistą jadalni: jeśli wątek zawsze najpierw bierze nóż, a potem widelec, nie będzie problemów z zakleszczeniem. Pierwszy strumień, który złapie nóż, będzie mógł normalnie jeść. W tłumaczeniu na język strumieni programu oznacza to, że przechwycenie obiektu 2 jest możliwe tylko wtedy, gdy obiekt 1 zostanie przechwycony po raz pierwszy.

    Ryż. 10.7. Analiza zakleszczeń w oknie wątku

    Dlatego jeśli usuniemy wywołanie Rnd w linii 98 i zastąpimy je fragmentem

    mFork.GrabFork (ja)

    mKnife.GrabKnife (ja)

    impas znika!

    Współpracuj na danych podczas ich tworzenia

    W aplikacjach wielowątkowych często dochodzi do sytuacji, w których wątki nie tylko pracują z udostępnionymi danymi, ale także czekają na ich pojawienie się (czyli wątek 1 musi utworzyć dane, zanim wątek 2 będzie mógł ich użyć). Ponieważ dane są udostępniane, dostęp do nich musi być zsynchronizowany. Niezbędne jest również zapewnienie środków do powiadamiania oczekujących wątków o pojawieniu się gotowych danych.

    Ta sytuacja jest zwykle nazywana problem dostawcy / konsumenta. Wątek próbuje uzyskać dostęp do danych, które jeszcze nie istnieją, więc musi przekazać kontrolę do innego wątku, który tworzy wymagane dane. Problem rozwiązuje następujący kod:

    • Wątek 1 (konsument) budzi się, wprowadza zsynchronizowaną metodę, wyszukuje dane, nie znajduje ich i przechodzi w stan oczekiwania. Wstępniefizycznie musi usunąć blokadę, aby nie zakłócać pracy wątku zasilającego.
    • Wątek 2 (dostawca) wchodzi w zsynchronizowaną metodę uwolnioną przez wątek 1, tworzy dane dla strumienia 1 i w jakiś sposób powiadamia strumień 1 o obecności danych. Następnie zwalnia blokadę, aby wątek 1 mógł przetworzyć nowe dane.

    Nie próbuj rozwiązywać tego problemu przez ciągłe wywoływanie wątku 1 i sprawdzanie warunku zmiennej warunku, której wartość jest > ustawiona przez wątek 2. Ta decyzja poważnie wpłynie na wydajność twojego programu, ponieważ w większości przypadków wątek 1 będzie być przywoływanym bez powodu; a wątek 2 będzie czekał tak często, że zabraknie mu czasu na utworzenie danych.

    Relacje dostawca/konsument są bardzo powszechne, dlatego dla takich sytuacji w wielowątkowych bibliotekach klas programowania tworzone są specjalne prymitywy. W NET te prymitywy nazywają się Wait i Pulse-PulseAl 1 i są częścią klasy Monitor. Rysunek 10.8 ilustruje sytuację, którą zamierzamy zaprogramować. Program organizuje trzy kolejki wątków: kolejkę oczekiwania, kolejkę blokującą i kolejkę wykonania. Harmonogram wątków nie przydziela czasu procesora wątkom, które znajdują się w oczekującej kolejce. Aby wątek miał przydzielony czas, musi przejść do kolejki wykonania. W rezultacie praca aplikacji jest zorganizowana znacznie wydajniej niż przy zwykłym odpytywaniu zmiennej warunkowej.

    W pseudokodzie idiom konsumenta danych jest sformułowany w następujący sposób:

    „Wejście do zsynchronizowanego bloku następującego typu

    Chociaż brak danych

    Przejdź do kolejki oczekujących

    Pętla

    Jeśli są dane, przetwórz je.

    Opuść zsynchronizowany blok

    Natychmiast po wykonaniu polecenia Wait wątek zostaje zawieszony, blokada zostaje zwolniona, a wątek przechodzi do kolejki oczekiwania. Po zwolnieniu blokady wątek w kolejce wykonania może działać. Z biegiem czasu jeden lub więcej zablokowanych wątków utworzy dane niezbędne do działania wątku znajdującego się w kolejce oczekiwania. Ponieważ walidacja danych odbywa się w pętli, przejście do korzystania z danych (po pętli) następuje tylko wtedy, gdy są dane gotowe do przetwarzania.

    W pseudokodzie idiom dostawcy danych wygląda tak:

    "Wprowadzanie zsynchronizowanego bloku widoku

    Chociaż dane NIE są potrzebne

    Przejdź do kolejki oczekujących

    Inne produkuj dane

    Gdy dane są gotowe, zadzwoń do Pulse-PulseAll.

    aby przenieść jeden lub więcej wątków z kolejki blokującej do kolejki wykonania. Opuść zsynchronizowany blok (i wróć do kolejki uruchomień)

    Załóżmy, że nasz program symuluje rodzinę z jednym rodzicem, który zarabia pieniądze i dzieckiem, które je wydaje. Kiedy pieniądze się skończąokazuje się, że dziecko musi poczekać na przybycie nowej kwoty. Implementacja programowa tego modelu wygląda tak:

    1 opcja ściśle włączona

    2 Importuje system. Wątek

    Moduł 3 modułów

    4 Sub Główny ()

    5 Dim theFamily As New Family ()

    6 Rodzina.StartltsLife ()

    7 Koniec Sub

    8 Koniec fjodule

    9

    10 Rodzina klas publicznych

    11 Prywatne mMoney jako liczba całkowita

    12 Prywatny mTydzień jako liczba całkowita = 1

    13 Public Sub StartltsLife ()

    14 Dim aThreadStart jako nowy ThreadStarUAddressOf Me.Produce)

    15 Dim bThreadStart jako nowy ThreadStarUAddressOf Me.Consume)

    16 Przyciemnij aThread jako nowy wątek (aThreadStart)

    17 Przyciemnij bThread jako nowy wątek (bThreadStart)

    18 aThread.Name = "Produkuj"

    19 aWątek.Start ()

    20 bThread.Name = "Zużyj"

    21 bWątek. Początek ()

    22 Koniec Sub

    23 Własność publiczna TheWeek () As Integer

    24 Pobierz

    25 Powrót mtydzień

    26 Koniec Pobierz

    27 Ustaw (wartość ByVal jako liczba całkowita)

    28 mtydzień - Wartość

    29 Koniec zestawu

    30 Koniec nieruchomości

    31 Własność publiczna OurMoney () As Integer

    32 Pobierz

    33 Zwrot mMoney

    34 Koniec Pobierz

    35 Ustaw (wartość ByVal jako liczba całkowita)

    36 mln Pieniądze = Wartość

    37 Koniec zestawu

    38 Koniec własności

    39 Subprodukcje publiczne ()

    40 Wątek.Sen (500)

    41

    42 Monitor.Enter (ja)

    43 Do While Me.OurMoney> 0

    44 Monitor.czekaj (ja)

    45 Pętla

    46 Ja.NaszePieniądze = 1000

    47 Monitor.PulseWszystko (Ja)

    48 Wyjście monitora (ja)

    49 Pętla

    50 Koniec Sub

    51 Subkonsumpcja publiczna ()

    52 MsgBox („Jestem w wątku konsumpcyjnym”)

    53

    54 Monitor.Enter (ja)

    55 Dopóki Ja.Nasze Pieniądze = 0

    56 Monitor.czekaj (ja)

    57 Pętla

    58 Console.WriteLine ("Drogi rodzicu, właśnie spędziłem wszystkie twoje" & _

    pieniądze w tygodniu "i TheWeek)

    59 Tydzień + = 1

    60 Jeśli TheWeek = 21 * 52 Wtedy System.Environment.Exit (0)

    61 Ja.NaszePieniądze = 0

    62 Monitor.PulseWszystko (ja)

    63 Wyjście monitora (ja)

    64 Pętla

    65 Koniec Sub

    66 Koniec klasy

    Metoda StartltsLife (linie 13–22) przygotowuje się do uruchomienia strumieni produkcji i konsumpcji. Najważniejsza rzecz dzieje się w strumieniach Produkcja (linie 39-50) i Konsumpcja (linie 51-65). Procedura SubProduct sprawdza dostępność pieniędzy, a jeśli są, trafiają do kolejki oczekujących. W przeciwnym razie rodzic generuje pieniądze (linia 46) i powiadamia obiekty w kolejce oczekujących o zmianie sytuacji. Zauważ, że wywołanie funkcji Pulse-Pulse All jest skuteczne tylko wtedy, gdy blokada zostanie zwolniona poleceniem Monitor.Exit. I odwrotnie, procedura Sub Consume sprawdza dostępność pieniędzy, a jeśli nie ma pieniędzy, powiadamia o tym oczekującego rodzica. Linia 60 po prostu kończy program po 21 latach warunkowych; wywołanie systemu. Environment.Exit (0) jest odpowiednikiem .NET polecenia End (polecenie End jest również obsługiwane, ale w przeciwieństwie do System. Environment. Exit, nie zwraca kodu zakończenia do systemu operacyjnego).

    Wątki umieszczone w kolejce oczekujących muszą zostać zwolnione przez inne części programu. Z tego powodu wolimy używać PulseAll over Pulse. Ponieważ nie wiadomo z góry, który wątek zostanie aktywowany po wywołaniu Pulse 1, jeśli w kolejce jest stosunkowo mało wątków, możesz równie dobrze wywołać PulseAll.

    Wielowątkowość w programach graficznych

    Nasza dyskusja na temat wielowątkowości w aplikacjach z graficznym interfejsem użytkownika zaczyna się od przykładu wyjaśniającego, do czego służy wielowątkowość w aplikacjach z graficznym interfejsem użytkownika. Utwórz formularz za pomocą dwóch przycisków Start (btnStart) i Anuluj (btnCancel), jak pokazano na rys. 10.9. Kliknięcie przycisku Start generuje klasę zawierającą losowy ciąg 10 milionów znaków oraz metodę zliczania wystąpień litery „E” w tym długim ciągu. Zwróć uwagę na użycie klasy StringBuilder w celu wydajniejszego tworzenia długich ciągów.

    Krok 1

    Wątek 1 zauważa, że ​​nie ma dla niego danych. Wywołuje Wait, zwalnia blokadę i przechodzi do kolejki oczekiwania.



    Krok 2

    Po zwolnieniu blokady wątek 2 lub wątek 3 opuszcza kolejkę bloków i wchodzi do zsynchronizowanego bloku, uzyskując blokadę

    Krok 3

    Powiedzmy, że wątek 3 wchodzi do zsynchronizowanego bloku, tworzy dane i wywołuje Pulse-Pulse All.

    Natychmiast po wyjściu z bloku i zwolnieniu blokady, wątek 1 jest przenoszony do kolejki wykonania. Jeśli wątek 3 wywołuje Pluse, tylko jeden wchodzi do kolejki wykonaniathread, gdy wywoływana jest funkcja Pluse All, wszystkie wątki trafiają do kolejki wykonania.



    Ryż. 10.8. Problem dostawcy/konsumenta

    Ryż. 10.9. Wielowątkowość w prostej aplikacji GUI

    Importuje System.Text

    Losowe postacie klasy publicznej

    Prywatne m_Data jako StringBuilder

    Prywatny mjength, m_count As Integer

    Public Sub New (ByVal n As Integer)

    m_Długość = n -1

    m_Data = Nowy StringBuilder (m_length) MakeString ()

    Napis końcowy

    Prywatny podrzędny ciąg MakeString ()

    Dim i jako liczba całkowita

    Dim myRnd As New Losowo ()

    Dla i = 0 Do m_długość

    „Wygeneruj losową liczbę między 65 a 90,

    „zmień to na wielkie litery

    "i dołącz do obiektu StringBuilder

    m_Data.Append (Chr (myRnd.Next (65.90)))

    Następny

    Napis końcowy

    Liczba startów publicznych subskrypcji ()

    GetEes ()

    Napis końcowy

    Prywatni GetEes ()

    Dim i jako liczba całkowita

    Dla i = 0 Do m_długość

    Jeśli m_Data.Chars (i) = CChar ("E") Wtedy

    m_liczba + = 1

    Zakończ, jeśli następny

    m_CountDone = Prawda

    Napis końcowy

    Publiczny tylko do odczytu

    Właściwość GetCount () jako liczba całkowita Get

    Jeśli nie (m_CountDone) To

    Zwróć m_count

    Zakończ, jeśli

    Koniec Pobierz Koniec właściwości

    Publiczny tylko do odczytu

    Właściwość IsDone () jako Boolean Get

    Powrót

    m_CountDone

    Koniec Pobierz

    Koniec właściwości

    Koniec klasy

    Z dwoma przyciskami na formularzu jest powiązany bardzo prosty kod. Procedura btn-Start_Click tworzy instancję powyższej klasy RandomCharacters, która hermetyzuje łańcuch zawierający 10 milionów znaków:

    Private Sub btnStart_Click (nadawca ByVal jako System.Object.

    ByVal e Jako System.EventArgs) Obsługuje btnSTART.Click

    Dim RC jako nowe losowe znaki (10000000)

    RC.StartCount ()

    MsgBox ("Liczba es to" & RC.GetCount)

    Napis końcowy

    Przycisk Anuluj wyświetla okno komunikatu:

    Private Sub btnCancel_Click (nadawca ByVal jako System.Object._

    ByVal e jako System.EventArgs) Obsługuje btnCancel.Click

    MsgBox („Odliczanie przerwane!”)

    Napis końcowy

    Po uruchomieniu programu i naciśnięciu przycisku Start okazuje się, że przycisk Anuluj nie reaguje na dane wejściowe użytkownika, ponieważ ciągła pętla uniemożliwia przyciskowi obsługę odbieranego zdarzenia. Jest to nie do przyjęcia w nowoczesnych programach!

    Możliwe są dwa rozwiązania. Pierwsza opcja, dobrze znana z poprzednich wersji VB, rezygnuje z wielowątkowości: wywołanie DoEvents jest zawarte w pętli. W NET to polecenie wygląda tak:

    Application.DoEvents ()

    W naszym przykładzie jest to zdecydowanie niepożądane — kto chce spowolnić program z dziesięcioma milionami wywołań DoEvents! Jeśli zamiast tego przydzielisz pętlę do oddzielnego wątku, system operacyjny przełączy się między wątkami, a przycisk Anuluj pozostanie funkcjonalny. Implementacja z osobnym wątkiem jest pokazana poniżej. Aby wyraźnie pokazać, że przycisk Anuluj działa, po jego kliknięciu po prostu zamykamy program.

    Następny krok: Pokaż przycisk liczenia

    Powiedzmy, że zdecydowałeś się pokazać swoją twórczą wyobraźnię i nadać formie wygląd pokazany na ryc. 10.9. Uwaga: przycisk Pokaż licznik nie jest jeszcze dostępny.

    Ryż. 10.10. Formularz z zablokowanym przyciskiem

    Oczekuje się, że osobny wątek wykona liczenie i odblokuje niedostępny przycisk. Można to oczywiście zrobić; co więcej, takie zadanie pojawia się dość często. Niestety, nie będziesz w stanie działać w najbardziej oczywisty sposób — połączyć wątek pomocniczy z wątkiem GUI, zachowując link do przycisku ShowCount w konstruktorze lub nawet używając standardowego delegata. Innymi słowy, nigdy nie używaj opcji poniżej (podstawowe błędny linie są pogrubione).

    Losowe postacie klasy publicznej

    Prywatna m_0ata jako StringBuilder

    Prywatne m_CountDone jako Boolean

    Prywatna mjength. m_count jako liczba całkowita

    Prywatny m_Button jako Windows.Forms.Button

    Public Sub New (ByVa1 n As Integer, _

    ByVal b Jako Windows.Forms.Button)

    m_długość = n - 1

    m_Data = Nowy StringBuilder (mJength)

    m_Przycisk = b Ciąg znaków ()

    Napis końcowy

    Prywatny podrzędny ciąg MakeString ()

    Dim I jako liczba całkowita

    Dim myRnd As New Losowo ()

    Dla I = 0 Do m_długość

    m_Data.Append (Chr (myRnd.Next (65.90)))

    Następny

    Napis końcowy

    Liczba startów publicznych subskrypcji ()

    GetEes ()

    Napis końcowy

    Prywatni GetEes ()

    Dim I jako liczba całkowita

    Dla I = 0 Do mjength

    Jeśli m_Data.Chars (I) = CChar ("E") Wtedy

    m_liczba + = 1

    Zakończ, jeśli następny

    m_CountDone = Prawda

    m_Button.Enabled = Prawda

    Napis końcowy

    Publiczny tylko do odczytu

    Właściwość GetCount () jako liczba całkowita

    Dostwać

    Jeśli nie (m_CountDone) To

    Wyrzuć nowy wyjątek („Nie zliczaj jeszcze”) W innym przypadku

    Zwróć m_count

    Zakończ, jeśli

    Koniec Pobierz

    Koniec właściwości

    Właściwość publiczna tylko do odczytu IsDone () jako Boolean

    Dostwać

    Zwróć m_CountDone

    Koniec Pobierz

    Koniec właściwości

    Koniec klasy

    Jest prawdopodobne, że ten kod zadziała w niektórych przypadkach. Niemniej jednak:

    • Nie można zorganizować interakcji wątku drugorzędnego z wątkiem tworzącym GUI oczywiste znaczy.
    • Nigdy nie modyfikuj elementów w programach graficznych z innych strumieni programu. Wszystkie zmiany powinny nastąpić tylko w wątku, który utworzył GUI.

    Jeśli złamiesz te zasady, my Gwarantujemyże subtelne, subtelne błędy pojawią się w twoich wielowątkowych programach graficznych.

    Nie będzie również w stanie zorganizować interakcji obiektów za pomocą zdarzeń. Pracownik 06-event działa w tym samym wątku, co RaiseEvent, więc zdarzenia Ci nie pomogą.

    Mimo to zdrowy rozsądek podpowiada, że ​​aplikacje graficzne muszą mieć możliwość modyfikowania elementów z innego wątku. W NET Framework istnieje bezpieczny wątkowo sposób wywoływania metod aplikacji GUI z innego wątku. W tym celu używany jest specjalny typ delegata Method Invoker z przestrzeni nazw System.Windows. Formularze. Poniższy fragment kodu przedstawia nową wersję metody GetEes (zmienione linie pogrubione):

    Prywatni GetEes ()

    Dim I jako liczba całkowita

    Dla I = 0 Do m_długość

    Jeśli m_Data.Chars (I) = CChar ("E") Wtedy

    m_liczba + = 1

    Zakończ, jeśli następny

    m_CountDone = Prawdziwa próba

    Dim mylnvoker jako nowy Methodlnvoker (AddressOf UpDateButton)

    myInvoker.Invoke () Złap e jako ThreadlnterruptedException

    "Niepowodzenie

    Koniec prób

    Napis końcowy

    Publiczny przycisk aktualizacji podrzędnej ()

    m_Button.Enabled = Prawda

    Napis końcowy

    Wywołania między wątkami przycisku nie są wykonywane bezpośrednio, ale za pośrednictwem Method Invoker. .NET Framework gwarantuje, że ta opcja jest bezpieczna wątkowo.

    Dlaczego jest tak wiele problemów z programowaniem wielowątkowym?

    Teraz, gdy masz już pewne pojęcie o wielowątkowości i potencjalnych problemach z nią związanych, zdecydowaliśmy, że właściwe będzie udzielenie odpowiedzi na pytanie w nagłówku tego podrozdziału na końcu tego rozdziału.

    Jednym z powodów jest to, że wielowątkowość jest procesem nieliniowym i jesteśmy przyzwyczajeni do liniowego modelu programowania. Na początku trudno przyzwyczaić się do samej idei, że wykonywanie programu może być losowo przerywane, a sterowanie przeniesione na inny kod.

    Jest jednak inny, bardziej fundamentalny powód: w dzisiejszych czasach programiści zbyt rzadko programują w asemblerze, a przynajmniej patrzą na zdeasemblowane dane wyjściowe kompilatora. W przeciwnym razie znacznie łatwiej byłoby im przyzwyczaić się do idei, że dziesiątki instrukcji asemblera mogą odpowiadać jednemu poleceniu języka wysokiego poziomu (takiego jak VB .NET). Wątek może zostać przerwany po wykonaniu dowolnej z tych instrukcji, a zatem w środku polecenia wysokiego poziomu.

    Ale to nie wszystko: nowoczesne kompilatory optymalizują wydajność programu, a sprzęt komputerowy może zakłócać zarządzanie pamięcią. Dzięki temu kompilator lub sprzęt może zmienić kolejność poleceń określoną w kodzie źródłowym programu bez Twojej wiedzy [ Wiele kompilatorów optymalizuje cykliczne kopiowanie tablic, jak dla i = 0 do n: b (i) = a (i): ncxt. Kompilator (a nawet wyspecjalizowany menedżer pamięci) może po prostu utworzyć tablicę, a następnie wypełnić ją pojedynczą operacją kopiowania zamiast wielokrotnie kopiować poszczególne elementy!].

    Mamy nadzieję, że te wyjaśnienia pomogą ci lepiej zrozumieć, dlaczego programowanie wielowątkowe powoduje tak wiele problemów - lub przynajmniej mniej zaskoczenia dziwnym zachowaniem twoich programów wielowątkowych!

    Jaki temat rodzi najwięcej pytań i trudności dla początkujących? Kiedy zapytałem o to mojego nauczyciela i programistę Java Aleksandra Priakhina, od razu odpowiedział: „Wielowątkowość”. Dzięki niemu za pomysł i pomoc w przygotowaniu tego artykułu!

    Zajrzymy do wewnętrznego świata aplikacji i jej procesów, dowiemy się, na czym polega istota wielowątkowości, kiedy jest przydatna i jak ją zaimplementować - na przykładzie Javy. Jeśli uczysz się innego języka OOP, nie martw się: podstawowe zasady są takie same.

    O strumieniach i ich pochodzeniu

    Aby zrozumieć wielowątkowość, najpierw zrozummy, czym jest proces. Proces to część pamięci wirtualnej i zasobów, które system operacyjny przydziela do uruchamiania programu. Jeśli otworzysz kilka instancji tej samej aplikacji, system przydzieli każdemu proces. W nowoczesnych przeglądarkach za każdą kartę może odpowiadać osobny proces.

    Zapewne zetknąłeś się z Windowsowym "Menedżerem zadań" (w Linuksie jest to "Monitor systemu") i wiesz, że niepotrzebne uruchomione procesy ładują system, a najbardziej "ciężkie" z nich często się zawieszają, więc trzeba je na siłę zakończyć .

    Ale użytkownicy uwielbiają wielozadaniowość: nie karm ich chlebem — wystarczy otworzyć kilkanaście okien i skakać tam iz powrotem. Pojawia się dylemat: trzeba zapewnić jednoczesne działanie aplikacji i jednocześnie zmniejszyć obciążenie systemu, aby nie zwalniał. Powiedzmy, że sprzęt nie nadąża za potrzebami właścicieli - musisz rozwiązać problem na poziomie oprogramowania.

    Chcemy, aby procesor wykonywał więcej instrukcji i przetwarzał więcej danych w jednostce czasu. Oznacza to, że w każdym wycinku czasu musimy zmieścić więcej wykonywanego kodu. Pomyśl o jednostce wykonania kodu jak o obiekcie — to jest wątek.

    Do złożonej sprawy łatwiej podejść, jeśli podzielisz ją na kilka prostych. Tak więc podczas pracy z pamięcią: „ciężki” proces dzieli się na wątki, które zajmują mniej zasobów i z większym prawdopodobieństwem dostarczą kod do kalkulatora (jak dokładnie – patrz niżej).

    Każda aplikacja ma co najmniej jeden proces, a każdy proces ma co najmniej jeden wątek, który nazywa się wątkiem głównym iz którego w razie potrzeby uruchamiane są nowe.

    Różnica między wątkami a procesami

      Wątki używają pamięci przydzielonej dla procesu, a procesy wymagają własnego miejsca w pamięci. Dzięki temu wątki są tworzone i kończone szybciej: system nie musi za każdym razem przydzielać im nowej przestrzeni adresowej, a następnie zwalniać jej.

      Każdy z procesów pracuje z własnymi danymi - mogą coś wymieniać tylko poprzez mechanizm komunikacji międzyprocesowej. Wątki uzyskują bezpośredni dostęp do swoich danych i zasobów: to, co zostało zmienione, jest natychmiast dostępne dla wszystkich. Wątek może kontrolować „kolegę” w procesie, podczas gdy proces kontroluje wyłącznie swoje „córki”. Dlatego przełączanie między strumieniami jest szybsze, a komunikacja między nimi łatwiejsza.

    Jaki jest wniosek z tego? Jeśli chcesz przetworzyć dużą ilość danych tak szybko, jak to możliwe, podziel je na porcje, które mogą być przetwarzane przez oddzielne wątki, a następnie połącz wynik. To lepsze niż rozmnażanie procesów zasobożernych.

    Ale dlaczego popularna aplikacja, taka jak Firefox, podąża drogą tworzenia wielu procesów? Ponieważ to dla przeglądarki izolowane karty działają niezawodnie i elastycznie. Jeśli coś jest nie tak z jednym procesem, nie trzeba przerywać całego programu - można zapisać przynajmniej część danych.

    Co to jest wielowątkowość

    Dochodzimy więc do głównego punktu. Wielowątkowość ma miejsce, gdy proces aplikacji jest podzielony na wątki, które są przetwarzane równolegle — w jednej jednostce czasu — przez procesor.

    Obciążenie obliczeniowe jest rozłożone na dwa lub więcej rdzeni, dzięki czemu interfejs i inne komponenty programu nie spowalniają nawzajem swojej pracy.

    Aplikacje wielowątkowe mogą być uruchamiane na procesorach jednordzeniowych, ale wtedy wątki są wykonywane po kolei: pierwszy działał, jego stan został zapisany - drugi mógł działać, zapisany - wrócił do pierwszego lub uruchomił trzeci, itp.

    Zapracowani ludzie narzekają, że mają tylko dwie ręce. Procesy i programy mogą mieć tyle rąk, ile potrzeba, aby wykonać zadanie tak szybko, jak to możliwe.

    Poczekaj na sygnał: synchronizacja w aplikacjach wielowątkowych

    Wyobraź sobie, że kilka wątków próbuje jednocześnie zmienić ten sam obszar danych. Czyje zmiany zostaną ostatecznie zaakceptowane, a czyje zmiany anulowane? Aby uniknąć pomyłek ze współdzielonymi zasobami, wątki muszą koordynować swoje działania. W tym celu wymieniają informacje za pomocą sygnałów. Każdy wątek mówi innym, co robi i jakich zmian można się spodziewać. Tak więc dane wszystkich wątków o aktualnym stanie zasobów są synchronizowane.

    Podstawowe narzędzia do synchronizacji

    Wzajemne wykluczenie (wykluczenie wzajemne, w skrócie - mutex) - "flaga", przejście do wątku, który w danej chwili ma prawo do pracy ze współdzielonymi zasobami. Eliminuje dostęp innych wątków do zajętego obszaru pamięci. W aplikacji może istnieć kilka muteksów, które mogą być współdzielone między procesami. Jest pewien haczyk: mutex wymusza na aplikacji dostęp do jądra systemu operacyjnego za każdym razem, co jest kosztowne.

    Semafor - pozwala ograniczyć liczbę wątków, które mogą w danym momencie uzyskać dostęp do zasobu. Zmniejszy to obciążenie procesora podczas wykonywania kodu, w którym występują wąskie gardła. Problem polega na tym, że optymalna liczba wątków zależy od maszyny użytkownika.

    Wydarzenie - definiujesz warunek, po wystąpieniu którego kontrola jest przekazywana do żądanego wątku. Strumienie wymieniają dane o zdarzeniach, aby rozwijać i logicznie kontynuować swoje działania. Jeden otrzymał dane, drugi sprawdził ich poprawność, trzeci zapisał je na twardym dysku. Wydarzenia różnią się sposobem ich odwoływania. Jeśli chcesz powiadomić kilka wątków o zdarzeniu, będziesz musiał ręcznie ustawić funkcję anulowania, aby zatrzymać sygnał. Jeśli istnieje tylko jeden wątek docelowy, możesz utworzyć zdarzenie automatycznego resetowania. Zatrzyma sam sygnał po dotarciu do strumienia. Zdarzenia można umieszczać w kolejce w celu elastycznej kontroli przepływu.

    Krytyczny fragment - bardziej złożony mechanizm, który łączy licznik pętli i semafor. Licznik umożliwia odroczenie uruchomienia semafora o żądany czas. Zaletą jest to, że jądro jest aktywowane tylko wtedy, gdy sekcja jest zajęta i semafor musi być włączony. Przez resztę czasu wątek działa w trybie użytkownika. Niestety sekcja może być używana tylko w ramach jednego procesu.

    Jak zaimplementować wielowątkowość w Javie

    Za pracę z wątkami w Javie odpowiada klasa Thread. Utworzenie nowego wątku do wykonania zadania oznacza utworzenie instancji klasy Thread i powiązanie jej z żądanym kodem. Można to zrobić na dwa sposoby:

      podklasa Wątek;

      zaimplementuj interfejs Runnable w swojej klasie, a następnie przekaż instancje klasy do konstruktora Thread.

    Chociaż nie poruszymy tematu zakleszczeń, gdy wątki blokują nawzajem swoją pracę i zawieszają się, zostawimy to na następny artykuł.

    Przykład wielowątkowości w Javie: ping-pong z muteksami

    Jeśli myślisz, że wydarzy się coś strasznego, zrób wydech. Rozważymy pracę z obiektami synchronizacji prawie w zabawny sposób: dwa wątki zostaną wyrzucone przez muteks.Ale w rzeczywistości zobaczysz prawdziwą aplikację, w której tylko jeden wątek może przetwarzać publicznie dostępne dane na raz.

    Najpierw stwórzmy klasę, która dziedziczy właściwości znanego już Threada i napiszmy metodę kickBall:

    Klasa publiczna PingPongThread extends Thread (PingPongThread (String name) (this.setName (nazwa); // nadpisz nazwę wątku) @Override public void run () (Ball ball = Ball.getBall (); while (ball.isInGame ()) ) (kickBall (piłka);)) private void kickBall (ball ball) (if (! ball.getSide (). equals (getName ())) (ball.kick (getName ());)))

    Teraz zajmijmy się piłką. U nas nie będzie prosty, ale zapadnie w pamięć: żeby mógł powiedzieć, kto go uderzył, z której strony i ile razy. W tym celu korzystamy z muteksu: zbierze on informacje o pracy każdego z wątków - pozwoli to na komunikowanie się ze sobą izolowanych wątków. Po 15 uderzeniu wyjmiemy piłkę z gry, aby nie poważnie jej zranić.

    Klasa publiczna Ball (prywatne int kicks = 0; prywatna statyczna instancja Ball = new Ball (); prywatna strona String = ""; private Ball () () static Ball getBall () (instancja zwrotna;) zsynchronizowane void kick (string playername) (kopnięcia ++; strona = nazwa gracza; System.out.println (kopnięcia + "" + strona);) String getSide () (strona powrotu;) boolean isInGame () (powrót (kopnięcia)< 15); } }

    A teraz na scenę wkraczają dwa wątki graczy. Nazwijmy je bez zbędnych ceregieli Ping and Pong:

    Klasa publiczna PingPongGame (PingPongThread player1 = new PingPongThread („Ping”); PingPongThread player2 = new PingPongThread („Pong”); Ball ball; PingPongGame () (ball = Ball.getBall ();) void startGame () rzuca InterruptedException (player1 .start (); player2.start ();))

    "Pełny stadion ludzi - czas zacząć mecz." Oficjalnie ogłosimy otwarcie spotkania - w głównej klasie aplikacji:

    Klasa publiczna PingPong (publiczna static void main (args String) rzuca InterruptedException (PingPongGame game = new PingPongGame (); game.startGame ();))

    Jak widać, nie ma tu nic wściekłego. To na razie tylko wstęp do wielowątkowości, ale już wiesz, jak to działa i możesz poeksperymentować - ogranicz czas gry nie liczbą uderzeń, ale np. czasem. Do tematu wielowątkowości wrócimy później — przyjrzymy się pakietowi java.util.concurrent, bibliotece Akka i mechanizmowi volatile. Porozmawiajmy też o implementacji wielowątkowości w Pythonie.

    Nożyce do gliny

    Wstęp

    Metody implementacji wielowątkowości firmy Intel obejmują cztery główne fazy: analizę, projektowanie i implementację, debugowanie i dostrajanie wydajności. Jest to podejście używane do tworzenia aplikacji wielowątkowych z kodu sekwencyjnego. Praca z oprogramowaniem na pierwszym, trzecim i czwartym etapie jest dość szeroko omówiona, natomiast informacje o realizacji drugiego etapu są wyraźnie niewystarczające.

    Opublikowano wiele książek na temat algorytmów równoległych i obliczeń równoległych. Jednak publikacje te dotyczą głównie przekazywania komunikatów, systemów pamięci rozproszonej lub teoretycznych modeli obliczeń równoległych, których czasami nie można zastosować na rzeczywistych platformach wielordzeniowych. Jeśli jesteś gotowy, aby poważnie podejść do programowania wielowątkowego, prawdopodobnie musisz wiedzieć, jak projektować algorytmy dla tych modeli. Oczywiście wykorzystanie tych modeli jest dość ograniczone, więc wielu programistów może być zmuszonych do ich praktycznego wdrożenia.

    Nie będzie przesadą stwierdzenie, że rozwój aplikacji wielowątkowych to przede wszystkim działalność twórcza, a dopiero potem naukowa. W tym artykule poznasz osiem prostych reguł, które pomogą Ci poszerzyć bazę praktyk programowania współbieżnego i poprawić efektywność wątkowania aplikacji.

    Zasada 1. Wybierz operacje wykonywane w kodzie programu niezależnie od siebie

    Przetwarzanie równoległe dotyczy tylko tych operacji w kodzie sekwencyjnym, które są wykonywane niezależnie od siebie. Dobrym przykładem tego, jak niezależne działania prowadzą do rzeczywistego pojedynczego rezultatu, jest budowanie domu. Skupia pracowników wielu specjalności: stolarzy, elektryków, tynkarzy, hydraulików, dekarzy, malarzy, murarzy, ogrodników itp. Oczywiście niektórzy z nich nie mogą rozpocząć pracy, zanim inni nie skończą swoich prac (np. dekarze nie zaczną pracy, dopóki ściany nie zostaną zbudowane, a malarze nie będą malować tych ścian, jeśli nie są otynkowane). Ale ogólnie można powiedzieć, że wszystkie osoby zaangażowane w budowę działają niezależnie od siebie.

    Rozważmy inny przykład – cykl pracy wypożyczalni DVD, która przyjmuje zamówienia na określone filmy. Zamówienia są dystrybuowane wśród pracowników punktu, którzy szukają tych filmów w magazynie. Oczywiście, jeśli któryś z pracowników wyjmie z magazynu płytę, na której nagrano film z Audrey Hepburn, w żaden sposób nie wpłynie to na innego pracownika szukającego kolejnego filmu akcji z Arnoldem Schwarzeneggerem, a tym bardziej nie wpłynie to na jego kolegę, który poszukuje płyt z nowym sezonem serialu „Przyjaciele”. W naszym przykładzie uważamy, że wszystkie problemy związane z brakiem folii na magazynie zostały rozwiązane zanim zamówienia dotarły do ​​wypożyczalni, a pakowanie i wysyłka żadnego zamówienia nie wpłyną na realizację pozostałych.

    W swojej pracy prawdopodobnie natkniesz się na obliczenia, które mogą być przetwarzane tylko w określonej kolejności, a nie równolegle, ponieważ różne iteracje lub kroki pętli są od siebie zależne i muszą być wykonywane w ściśle określonej kolejności. Weźmy żywy przykład z natury. Wyobraź sobie ciężarną sarnę. Ponieważ urodzenie płodu trwa średnio osiem miesięcy, cokolwiek by powiedzieć, jelonek nie pojawi się w ciągu miesiąca, nawet jeśli osiem reniferów zajdzie w ciążę w tym samym czasie. Jednak osiem reniferów w tym samym czasie doskonale wykonałoby swoje zadanie, gdyby było zaprzęgnięte do nich wszystkich w saniach Mikołaja.

    Zasada 2. Zastosuj równoległość z niskim poziomem szczegółowości

    Istnieją dwa podejścia do równoległego partycjonowania kodu programu sekwencyjnego: oddolne i odgórne. Najpierw na etapie analizy kodu wyznaczane są segmenty kodu (tzw. „gorące” punkty), które zajmują znaczną część czasu wykonania programu. Rozdzielenie tych segmentów kodu równolegle (jeśli to możliwe) zapewni maksymalny wzrost wydajności.

    Podejście oddolne implementuje wielowątkowość gorących punktów kodu. Jeśli nie jest możliwe równoległe dzielenie znalezionych punktów, należy sprawdzić stos wywołań aplikacji, aby określić inne segmenty, które są dostępne do dzielenia równoległego, a ich ukończenie zajmuje dużo czasu. Załóżmy, że pracujesz nad aplikacją do kompresji grafiki. Kompresja może być realizowana za pomocą kilku niezależnych równoległych strumieni przetwarzających poszczególne segmenty obrazu. Jednak nawet jeśli udało Ci się zaimplementować wielowątkowość „gorących” miejsc, nie zaniedbuj analizy stosu wywołań, w wyniku której możesz znaleźć segmenty dostępne do podziału równoległego na wyższym poziomie kodu programu. W ten sposób możesz zwiększyć szczegółowość przetwarzania równoległego.

    W podejściu odgórnym analizowana jest praca kodu programu, a poszczególne jego segmenty są wyróżniane, których wykonanie prowadzi do zakończenia całego zadania. Jeśli nie ma wyraźnej niezależności głównych segmentów kodu, przeanalizuj ich części składowe, aby znaleźć niezależne obliczenia. Analizując kod programu, możesz zidentyfikować moduły kodu, które zużywają najwięcej czasu procesora. Przyjrzyjmy się, jak zaimplementować wątki w aplikacji do kodowania wideo. Przetwarzanie równoległe może być realizowane na najniższym poziomie - dla niezależnych pikseli jednej klatki lub na wyższym poziomie - dla grup ramek, które mogą być przetwarzane niezależnie od innych grup. Jeśli aplikacja jest pisana w celu przetwarzania wielu plików wideo w tym samym czasie, dzielenie równoległe na tym poziomie może być jeszcze łatwiejsze, a szczegółowość będzie najmniejsza.

    Szczegółowość obliczeń równoległych odnosi się do ilości obliczeń, które należy wykonać przed synchronizacją między wątkami. Innymi słowy, im rzadziej występuje synchronizacja, tym niższa ziarnistość. Szczegółowe obliczenia dotyczące wątków mogą spowodować, że obciążenie systemu związane z wątkowaniem przeważy nad użytecznymi obliczeniami wykonywanymi przez te wątki. Wzrost liczby wątków przy tej samej ilości obliczeń komplikuje proces przetwarzania. Wielowątkowość o niskiej szczegółowości wprowadza mniejsze opóźnienia systemu i ma większy potencjał skalowalności, co można osiągnąć za pomocą dodatkowych wątków. Aby zaimplementować przetwarzanie równoległe o niskiej ziarnistości, zaleca się użycie podejścia odgórnego i wątku na wysokim poziomie w stosie wywołań.

    Zasada 3: Wbuduj skalowalność w swój kod, aby poprawić wydajność wraz ze wzrostem liczby rdzeni.

    Nie tak dawno, oprócz procesorów dwurdzeniowych, na rynku pojawiły się czterordzeniowe. Co więcej, Intel już zapowiedział procesor z 80 rdzeniami, zdolny do wykonywania bilionów operacji zmiennoprzecinkowych na sekundę. Ponieważ liczba rdzeni w procesorach będzie rosła z czasem, Twój kod musi mieć odpowiedni potencjał skalowalności. Skalowalność to parametr, za pomocą którego można ocenić zdolność aplikacji do odpowiedniego reagowania na zmiany, takie jak wzrost zasobów systemowych (liczba rdzeni, rozmiar pamięci, częstotliwość magistrali itp.) lub zwiększenie ilości danych. Wraz ze wzrostem liczby rdzeni w przyszłych procesorach należy pisać skalowalny kod, który zwiększy wydajność poprzez zwiększenie zasobów systemowych.

    Parafrazując jedno z praw Northcote Parkinson (C. Northecote Parkinson), możemy powiedzieć, że „przetwarzanie danych zajmuje wszystkie dostępne zasoby systemowe”. Oznacza to, że wraz ze wzrostem zasobów obliczeniowych (np. liczby rdzeni) wszystkie z nich najprawdopodobniej zostaną wykorzystane do przetwarzania danych. Wróćmy do omówionej powyżej aplikacji do kompresji wideo. Dodanie dodatkowych rdzeni do procesora raczej nie wpłynie na rozmiar przetwarzanych klatek - zamiast tego wzrośnie liczba wątków przetwarzających ramkę, co doprowadzi do zmniejszenia liczby pikseli na wątek. W rezultacie, ze względu na organizację dodatkowych strumieni, ilość narzutu wzrośnie, a stopień ziarnistości równoległości zmniejszy się. Innym bardziej prawdopodobnym scenariuszem jest zwiększenie rozmiaru lub liczby plików wideo, które należy zakodować. W takim przypadku zorganizowanie dodatkowych strumieni, które będą przetwarzać większe (lub dodatkowe) pliki wideo, pozwoli na rozłożenie całego wolumenu pracy bezpośrednio na etapie, na którym nastąpił wzrost. Z kolei aplikacja o takich możliwościach będzie miała duży potencjał skalowalności.

    Projektowanie i implementacja przetwarzania równoległego z wykorzystaniem dekompozycji danych zapewnia większą skalowalność w porównaniu z wykorzystaniem dekompozycji funkcjonalnej. Ilość niezależnych funkcji w kodzie programu jest najczęściej ograniczona i nie zmienia się w trakcie wykonywania aplikacji. Ponieważ każda niezależna funkcja ma oddzielny wątek (i odpowiednio rdzeń procesora), wraz ze wzrostem liczby rdzeni dodatkowo zorganizowane wątki nie spowodują wzrostu wydajności. Tak więc modele partycjonowania równoległego z dekompozycją danych zapewnią zwiększony potencjał skalowalności aplikacji ze względu na fakt, że ilość przetwarzanych danych będzie rosła wraz z liczbą rdzeni procesora.

    Nawet jeśli kod programu wątkuje w niezależne funkcje, jest prawdopodobne, że dodatkowe wątki mogą zostać użyte do uruchomienia, gdy zwiększy się obciążenie wejściowe. Wróćmy do omówionego powyżej przykładu budowy domu. Celem konstrukcji jest wykonanie ograniczonej liczby samodzielnych zadań. Jeśli jednak zostaniesz poinstruowany, aby zbudować dwa razy więcej pięter, prawdopodobnie będziesz chciał zatrudnić dodatkowych pracowników w niektórych specjalnościach (malarzy, dekarzy, hydraulików itp.). Dlatego trzeba tworzyć aplikacje, które potrafią dostosować się do dekompozycji danych wynikającej ze zwiększonego obciążenia pracą. Jeśli Twój kod implementuje dekompozycję funkcjonalną, rozważ zorganizowanie dodatkowych wątków w miarę wzrostu liczby rdzeni procesora.

    Zasada 4. Używaj bezpiecznych wątków bibliotek

    Jeśli możesz potrzebować biblioteki do obsługi punktów aktywnych danych w kodzie, rozważ użycie gotowych funkcji zamiast własnego kodu. Krótko mówiąc, nie próbuj wymyślać koła na nowo, rozwijając segmenty kodu, których funkcje są już zapewnione w zoptymalizowanych procedurach bibliotecznych. Wiele bibliotek, w tym Intel® Math Kernel Library (Intel® MKL) i Intel® Integrated Performance Primitives (Intel® IPP), zawiera już wielowątkową funkcjonalność zoptymalizowaną pod kątem procesorów wielordzeniowych.

    Warto zauważyć, że korzystając z procedur z bibliotek wielowątkowych należy upewnić się, że wywołanie konkretnej biblioteki nie wpłynie na normalne działanie wątków. Oznacza to, że jeśli wywołania procedur są wykonywane z dwóch różnych wątków, prawidłowe wyniki powinny być zwracane z każdego wywołania. Jeżeli procedury odwołują się do zmiennych z bibliotek współdzielonych i je aktualizują, może dojść do wyścigu danych, co niekorzystnie wpłynie na wiarygodność wyników obliczeń. Aby poprawnie pracować z wątkami, procedura biblioteki jest dodawana jako nowa (to znaczy nie aktualizuje niczego poza zmiennymi lokalnymi) lub synchronizowana w celu ochrony dostępu do współdzielonych zasobów. Wniosek: przed użyciem w kodzie jakiejkolwiek biblioteki innej firmy przeczytaj dołączoną do niej dokumentację, aby upewnić się, że działa poprawnie ze strumieniami.

    Zasada 5. Użyj odpowiedniego modelu wielowątkowości

    Załóżmy, że wyraźnie nie wystarczy funkcji z bibliotek wielowątkowych do równoległego rozdzielenia wszystkich odpowiednich segmentów kodu, a trzeba było pomyśleć o organizacji wątków. Nie spiesz się z tworzeniem własnej (kłopotliwej) struktury wątków, jeśli biblioteka OpenMP zawiera już wszystkie potrzebne funkcje.

    Wadą jawnej wielowątkowości jest niemożność precyzyjnej kontroli wątków.

    Jeśli potrzebujesz tylko równoległego oddzielenia pętli intensywnie korzystających z zasobów lub dodatkowa elastyczność zapewniana przez wątki jawne jest dla ciebie drugorzędna, w takim przypadku nie ma sensu wykonywać dodatkowej pracy. Im bardziej złożona implementacja wielowątkowości, tym większe prawdopodobieństwo błędów w kodzie i trudniejsze jego późniejsze dopracowanie.

    Biblioteka OpenMP koncentruje się na dekompozycji danych i jest szczególnie dobrze przystosowana do pętli wątków pracujących z dużymi ilościami informacji. Pomimo tego, że do niektórych aplikacji ma zastosowanie tylko dekompozycja danych, konieczne jest uwzględnienie dodatkowych wymagań (np. pracodawcy lub klienta), zgodnie z którymi korzystanie z OpenMP jest niedopuszczalne i pozostaje wdrożenie wielowątkowości przy użyciu jawnych metody. W takim przypadku OpenMP można wykorzystać do wstępnego wątkowania w celu oszacowania potencjalnego wzrostu wydajności, skalowalności i przybliżonego wysiłku, który byłby wymagany do późniejszego podziału kodu przy użyciu jawnej wielowątkowości.

    Zasada 6. Wynik kodu programu nie powinien zależeć od kolejności wykonywania równoległych wątków

    W przypadku sekwencyjnego kodu programu wystarczy po prostu zdefiniować wyrażenie, które zostanie wykonane po każdym innym wyrażeniu. W kodzie wielowątkowym kolejność wykonywania wątków nie jest zdefiniowana i zależy od instrukcji planisty systemu operacyjnego. Ściśle mówiąc, prawie niemożliwe jest przewidzenie sekwencji wątków, które zostaną uruchomione w celu wykonania operacji, lub określenie, który wątek zostanie uruchomiony przez harmonogram w późniejszym czasie. Przewidywanie służy głównie do zmniejszania opóźnień aplikacji, zwłaszcza gdy działa na platformie z procesorem, który ma mniej rdzeni niż liczba zorganizowanych wątków. Jeśli wątek jest zablokowany, ponieważ potrzebuje dostępu do obszaru, który nie jest zapisany w pamięci podręcznej, lub ponieważ musi wykonać żądanie We/Wy, harmonogram zawiesi go i uruchomi wątek gotowy do uruchomienia.

    Sytuacje wyścigu danych są natychmiastowym wynikiem niepewności w planowaniu wykonywania wątków. Założenie, że jakiś wątek zmieni wartość zmiennej współdzielonej, zanim inny wątek odczyta tę wartość, może być błędem. Przy odrobinie szczęścia kolejność wykonywania wątków dla danej platformy pozostanie taka sama we wszystkich uruchomieniach aplikacji. Jednak najmniejsze zmiany stanu systemu (na przykład lokalizacja danych na dysku twardym, szybkość pamięci, a nawet odchylenie od nominalnej częstotliwości zasilania AC) mogą wywołać inną kolejność wykonywania wątków. Dlatego w przypadku kodu programu, który działa poprawnie tylko z określoną sekwencją wątków, prawdopodobne są problemy związane z sytuacjami „wyścigu danych” i zakleszczeniami.

    Z punktu widzenia wzrostu wydajności lepiej nie ograniczać kolejności wykonywania wątków. Ścisła sekwencja wykonywania strumieni jest dozwolona tylko wtedy, gdy jest to absolutnie konieczne, określone z góry określonym kryterium. W przypadku wystąpienia takiej okoliczności wątki zostaną uruchomione w kolejności określonej przez dostarczone mechanizmy synchronizacji. Na przykład wyobraź sobie dwóch przyjaciół czytających gazetę rozłożoną na stole. Po pierwsze, mogą czytać z różną prędkością, a po drugie, mogą czytać różne artykuły. I tutaj nie ma znaczenia, kto pierwszy przeczyta rozkładówkę - w każdym razie będzie musiał poczekać na przyjaciela, zanim przewróci stronę. Jednocześnie nie ma ograniczeń co do czasu i kolejności czytania artykułów – znajomi czytają z dowolną prędkością, a synchronizacja między nimi następuje bezpośrednio podczas przewracania strony.

    Zasada 7. Używaj lokalnego magazynu strumieniowego. W razie potrzeby przypisz blokady do określonych obszarów danych

    Synchronizacja nieuchronnie zwiększa obciążenie systemu, co w żaden sposób nie przyspiesza procesu uzyskiwania wyników obliczeń równoległych, ale zapewnia ich poprawność. Tak, synchronizacja jest konieczna, ale nie należy jej nadużywać. Aby zminimalizować synchronizację, stosuje się lokalne przechowywanie strumieni lub przydzielone obszary pamięci (na przykład elementy tablicy oznaczone identyfikatorami odpowiednich strumieni).

    Potrzeba współdzielenia zmiennych tymczasowych przez różne wątki jest rzadka. Takie zmienne muszą być zadeklarowane lub przydzielone lokalnie do każdego wątku. Zmienne, których wartości są pośrednimi wynikami wykonania wątków, również muszą być zadeklarowane lokalnie dla odpowiednich wątków. Synchronizacja jest wymagana do zsumowania tych wyników pośrednich we wspólnym obszarze pamięci. Aby zminimalizować potencjalne obciążenie systemu, najlepiej aktualizować ten wspólny obszar w jak najmniejszym stopniu. W przypadku jawnych metod wielowątkowych udostępniane są interfejsy API lokalnego przechowywania wątków, które zapewniają integralność danych lokalnych od rozpoczęcia wykonywania jednego wielowątkowego segmentu kodu do początku następnego segmentu (lub podczas przetwarzania jednego wywołania funkcji wielowątkowej do kolejne wykonanie tej samej funkcji).

    Jeśli nie jest możliwe przechowywanie strumieni lokalnie, dostęp do udostępnionych zasobów jest synchronizowany przy użyciu różnych obiektów, takich jak blokady. W takim przypadku ważne jest prawidłowe przypisanie blokad do określonych bloków danych, co jest najłatwiejsze, jeśli liczba blokad jest równa liczbie bloków danych. Pojedynczy mechanizm blokujący, który synchronizuje dostęp do wielu obszarów pamięci, jest używany tylko wtedy, gdy wszystkie te obszary znajdują się stale w tej samej krytycznej sekcji kodu programu.

    Co zrobić, jeśli potrzebujesz zsynchronizować dostęp do dużej ilości danych, na przykład do tablicy 10 000 elementów? Zapewnienie jednej blokady dla całej macierzy jest zdecydowanie wąskim gardłem w aplikacji. Czy naprawdę musisz organizować blokowanie dla każdego elementu osobno? Wtedy, nawet jeśli 32 lub 64 równoległe wątki będą miały dostęp do danych, będziesz musiał zapobiegać konfliktom dostępu do dość dużego obszaru pamięci, a prawdopodobieństwo takich konfliktów wynosi 1%. Na szczęście istnieje rodzaj złotego środka, tzw. „blokada modulo”. Jeśli używanych jest N blokad modulo, każda z nich zsynchronizuje dostęp do N-tej części współdzielonego obszaru danych. Na przykład, jeśli zorganizowane są dwie takie blokady, to jedna z nich uniemożliwi dostęp do parzystych elementów tablicy, a druga - do nieparzystych. W takim przypadku wątki, odwołując się do wymaganego elementu, określają jego parzystość i ustawiają odpowiednią blokadę. Liczbę blokad modulo dobiera się biorąc pod uwagę liczbę wątków oraz prawdopodobieństwo równoczesnego dostępu kilku wątków do tego samego obszaru pamięci.

    Należy zauważyć, że jednoczesne użycie kilku mechanizmów blokujących nie pozwala na synchronizację dostępu do jednego obszaru pamięci. Przypomnijmy prawo Segala: „Człowiek, który ma tylko jeden zegarek, wie na pewno, która jest godzina. Osoba, która ma kilka godzin, nie jest niczego pewna.” Załóżmy, że dwie różne blokady kontrolują dostęp do zmiennej. W takim przypadku pierwszy zamek może być używany przez jeden segment kodu, a drugi przez inny segment. Następnie wątki wykonujące te segmenty znajdą się w sytuacji wyścigu o współdzielone dane, do których uzyskują dostęp w tym samym czasie.

    Zasada 8. Zmień algorytm oprogramowania, jeśli jest to wymagane do wdrożenia wielowątkowości

    Kryterium oceny wydajności aplikacji, zarówno sekwencyjnych, jak i równoległych, jest czas wykonania. Jako oszacowanie algorytmu odpowiedni jest porządek asymptotyczny. Ta teoretyczna metryka jest prawie zawsze przydatna do oceny wydajności aplikacji. Oznacza to, że jeśli wszystkie inne czynniki są równe, aplikacja o tempie wzrostu O (n log n) (szybkie sortowanie) będzie działać szybciej niż aplikacja o tempie wzrostu O (n2) (sortowanie selektywne), chociaż wyniki tych aplikacje są takie same.

    Im lepsza asymptotyczna kolejność wykonywania, tym szybciej działa aplikacja równoległa. Jednak nawet najbardziej wydajny algorytm sekwencyjny nie zawsze może zostać podzielony na równoległe strumienie. Jeśli punkt aktywny programu jest zbyt trudny do podzielenia i nie ma możliwości wielowątkowości na wyższym poziomie stosu wywołań punktu aktywnego, należy najpierw rozważyć użycie innego algorytmu sekwencyjnego, który jest łatwiejszy do podzielenia niż oryginalny. Oczywiście istnieją inne sposoby na przygotowanie kodu do obsługi wątków.

    Jako ilustrację ostatniego stwierdzenia rozważ mnożenie dwóch macierzy kwadratowych. Algorytm Strassena ma jedną z najlepszych asymptotycznych kolejności wykonywania: O (n2.81), która jest znacznie lepsza niż kolejność O (n3) zwykłego algorytmu potrójnej zagnieżdżonej pętli. Zgodnie z algorytmem Strassena, każda macierz jest dzielona na cztery podmacierze, po których wykonywanych jest siedem wywołań rekurencyjnych w celu pomnożenia n/2 × n/2 podmacierzy. Aby zrównoleglić wywołania rekurencyjne, możesz utworzyć nowy wątek, który będzie kolejno wykonywał siedem niezależnych mnożeń podmacierzy, aż osiągną określony rozmiar. W takim przypadku liczba wątków będzie rosła wykładniczo, a szczegółowość obliczeń wykonywanych przez każdy nowo utworzony wątek będzie rosła wraz ze zmniejszaniem się rozmiaru podmacierzy. Rozważ inną opcję - zorganizowanie puli siedmiu wątków pracujących jednocześnie i wykonanie jednego mnożenia podmacierzy. Po zakończeniu puli wątków rekursywnie wywoływana jest metoda Strassena w celu pomnożenia podmacierzy (jak w sekwencyjnej wersji kodu programu). Jeśli system, na którym działa taki program, ma więcej niż osiem rdzeni procesorów, niektóre z nich będą bezczynne.

    Algorytm mnożenia macierzy jest znacznie łatwiejszy do zrównoleglenia przy użyciu potrójnej pętli zagnieżdżonej. W tym przypadku stosowana jest dekompozycja danych, w której macierze są dzielone na wiersze, kolumny lub podmacierze, a każdy z wątków wykonuje określone obliczenia. Implementacja takiego algorytmu odbywa się za pomocą pragm OpenMP wstawionych na pewnym poziomie pętli lub poprzez jawne organizowanie wątków, które dokonują podziału na macierz. Implementacja tego prostszego algorytmu sekwencyjnego będzie wymagała znacznie mniej modyfikacji w kodzie programu w porównaniu z implementacją wielowątkowego algorytmu Strassena.

    Znasz teraz osiem prostych zasad efektywnej konwersji kodu sekwencyjnego na równoległy. Przestrzegając tych zasad, będziesz w stanie tworzyć rozwiązania wielowątkowe znacznie szybciej, z większą niezawodnością, optymalną wydajnością i mniejszą liczbą wąskich gardeł.

    Aby wrócić do strony internetowej z wielowątkowymi samouczkami programowania, przejdź do