Cyfroteka.pl

klikaj i czytaj online

Cyfro
Czytomierz
00360 005925 19029614 na godz. na dobę w sumie
Język C++ i przetwarzanie współbieżne w akcji. Wydanie II - książka
Język C++ i przetwarzanie współbieżne w akcji. Wydanie II - książka
Autor: Liczba stron: 640
Wydawca: Helion Język publikacji: polski
ISBN: 978-83-283-4448-8 Data wydania:
Lektor:
Kategoria: ebooki >> komputery i informatyka >> programowanie >> c++ - programowanie
Porównaj ceny (książka, ebook (-35%), audiobook).

Jeśli aplikacja ma działać szybko i niezawodnie, najlepiej wybrać C++, dojrzały i wszechstronny język programowania, konsekwentnie rozwijany przez mistrzów kodowania. Wymaga on zachowania pewnej dyscypliny podczas pracy, jednak pozwala na uzyskanie kodu o znakomitej wydajności. Nowy standard C++17 zapewnia doskonałą obsługę wielowątkowości oraz programowania wieloprocesorowego wymaganego podczas szybkiego przetwarzania grafiki, uczenia maszynowego czy też wykonywania innych zadań, w których kluczową sprawą okazuje się wydajność.

Ta książka jest drugim, zaktualizowanym i uzupełnionym wydaniem doskonałego podręcznika dla profesjonalistów. Szczegółowo opisano w niej wszystkie etapy programowania współbieżnego: od utworzenia wątków po projektowanie wielowątkowych algorytmów i struktur danych. Przedstawiono zastosowania klas std::thread i std::mutex oraz funkcji std::async, a także złożone zagadnienia związane z operacjami atomowymi i modelem pamięci. Sporo miejsca poświęcono diagnozowaniu kodu i analizie rodzajów błędów. Opisano techniki lokalizowania błędów oraz metody testowania kodu. Prezentowany materiał został uzupełniony przykładami kodu i praktycznymi ćwiczeniami. Znalazły się tu również porady i wskazówki, które docenią wszyscy programiści C++.

W tej książce między innymi:

Programuj elegancko, twórz wydajny i czysty kod. Oto współbieżność w C++!

Znajdź podobne książki Ostatnio czytane w tej kategorii

Darmowy fragment publikacji:

Tytuł oryginału: C++ Concurrency in Action, 2nd Edition Tłumaczenie: Robert Górczyński na podstawie: „Język C++ i przetwarzanie współbieżne w akcji” w przekładzie Mikołaja Szczepaniaka ISBN: 978-83-283-4448-8 Original edition copyright © 2019 by Manning Publications Co. All rights reserved. Polish edition copyright © 2020 by HELION SA. All rights reserved. All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from the Publisher. Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości lub fragmentu niniejszej publikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii metodą kserograficzną, fotograficzną, a także kopiowanie książki na nośniku filmowym, magnetycznym lub innym powoduje naruszenie praw autorskich niniejszej publikacji. Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi bądź towarowymi ich właścicieli. Autor oraz Helion SA dołożyli wszelkich starań, by zawarte w tej książce informacje były kompletne i rzetelne. Nie biorą jednak żadnej odpowiedzialności ani za ich wykorzystanie, ani za związane z tym ewentualne naruszenie praw patentowych lub autorskich. Autor oraz Helion SA nie ponoszą również żadnej odpowiedzialności za ewentualne szkody wynikłe z wykorzystania informacji zawartych w książce. Materiały graficzne na okładce zostały wykorzystane za zgodą Shutterstock Images LLC. Helion SA ul. Kościuszki 1c, 44-100 Gliwice tel. 32 231 22 19, 32 230 98 63 e-mail: helion@helion.pl WWW: http://helion.pl (księgarnia internetowa, katalog książek) Drogi Czytelniku! Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres http://helion.pl/user/opinie/jcppw2 Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję. Printed in Poland. • Kup książkę • Poleć książkę • Oceń książkę • Księgarnia internetowa • Lubię to! » Nasza społeczność Spis treści Rozdział 1. Witaj, świecie współbieżności w C++! Słowo wstępne .......................................................................................................................................... 11 Podziękowania .......................................................................................................................................... 13 O tej książce .............................................................................................................................................. 15 O autorze .................................................................................................................................................. 19 21 1.1. Czym jest współbieżność? ........................................................................................................... 22 1.1.1. Współbieżność w systemach komputerowych ................................................................. 22 1.1.2. Modele współbieżności ..................................................................................................... 25 1.1.3. Współbieżność kontra równoległość .................................................................................. 27 1.2. Dlaczego warto stosować współbieżność? ................................................................................. 27 1.2.1. Stosowanie współbieżności do podziału zagadnień ......................................................... 27 1.2.2. Stosowanie współbieżności do podniesienia wydajności — równoległość zadań i danych ........................................................................................ 28 1.2.3. Kiedy nie należy stosować współbieżności ....................................................................... 30 1.3. Współbieżność i wielowątkowość w języku C++ .................................................................... 31 1.3.1. Historia przetwarzania wielowątkowego w języku C++ ................................................ 31 1.3.2. Obsługa współbieżności w nowym standardzie ............................................................... 32 1.3.3. Większa obsługa współbieżności i równoległości w standardach C++14 i C++17 .... 33 1.3.4. Efektywność biblioteki wątków języka C++ .................................................................. 33 1.3.5. Mechanizmy związane z poszczególnymi platformami ................................................... 34 1.4. Do dzieła ....................................................................................................................................... 35 1.4.1. Witaj, świecie współbieżności! .......................................................................................... 35 1.5. Podsumowanie .............................................................................................................................. 36 39 2.1. Podstawowe zarządzanie wątkami ............................................................................................. 40 2.1.1 Uruchamianie wątku .......................................................................................................... 40 2.1.2. Oczekiwanie na zakończenie wątku .................................................................................. 43 2.1.3. Oczekiwanie w razie wystąpienia wyjątku ....................................................................... 44 2.1.4. Uruchamianie wątków w tle .............................................................................................. 46 2.2. Przekazywanie argumentów do funkcji wątku ......................................................................... 47 2.3. Przenoszenie własności wątku .................................................................................................... 50 2.4. Wybór liczby wątków w czasie wykonywania ........................................................................... 55 2.5. Identyfikowanie wątków ............................................................................................................. 57 2.6. Podsumowanie .............................................................................................................................. 59 Rozdział 2. Zarządzanie wątkami Poleć książkęKup książkę 6 Spis treści Rozdział 3. Współdzielenie danych przez wątki 61 3.1. Problemy związane ze współdzieleniem danych przez wątki ................................................. 62 3.1.1. Sytuacja wyścigu ................................................................................................................ 64 3.1.2. Unikanie problematycznych sytuacji wyścigu .................................................................. 65 3.2. Ochrona współdzielonych danych za pomocą muteksów ........................................................ 66 3.2.1. Stosowanie muteksów w języku C++ ............................................................................. 66 3.2.2. Projektowanie struktury kodu z myślą o ochronie współdzielonych danych ................. 68 3.2.3. Wykrywanie sytuacji wyścigu związanych z interfejsami ................................................ 70 3.2.4. Zakleszczenie: problem i rozwiązanie .............................................................................. 77 3.2.5. Dodatkowe wskazówki dotyczące unikania zakleszczeń ................................................. 80 3.2.6. Elastyczne blokowanie muteksów za pomocą szablonu std::unique_lock ...................... 87 3.2.7. Przenoszenie własności muteksu pomiędzy zasięgami .................................................... 89 3.2.8. Dobór właściwej szczegółowości blokad .......................................................................... 90 3.3. Alternatywne mechanizmy ochrony współdzielonych danych ................................................ 93 3.3.1. Ochrona współdzielonych danych podczas inicjalizacji .................................................. 93 3.3.2. Ochrona rzadko aktualizowanych struktur danych .......................................................... 97 3.3.3. Blokowanie rekurencyjne .................................................................................................. 99 3.4. Podsumowanie ............................................................................................................................ 100 101 4.1. Oczekiwanie na zdarzenie lub inny warunek ......................................................................... 102 4.1.1. Oczekiwanie na spełnienie warunku za pomocą zmiennych warunkowych ................ 103 4.1.2. Budowa kolejki gwarantującej bezpieczne przetwarzanie wielowątkowe Rozdział 4. Synchronizacja współbieżnych operacji przy użyciu zmiennych warunkowych ............................................................................ 106 4.2. Oczekiwanie na jednorazowe zdarzenia za pomocą przyszłości ........................................... 111 4.2.1. Zwracanie wartości przez zadania wykonywane w tle ................................................... 112 4.2.2. Wiązanie zadania z przyszłością ...................................................................................... 114 4.2.3. Obietnice (szablon std::promise) ..................................................................................... 117 4.2.4. Zapisywanie wyjątku na potrzeby przyszłości ................................................................ 119 4.2.5. Oczekiwanie na wiele wątków ........................................................................................ 121 4.3. Oczekiwanie z limitem czasowym ............................................................................................ 124 4.3.1. Zegary ............................................................................................................................... 124 4.3.2. Okresy ............................................................................................................................... 125 4.3.3. Punkty w czasie ................................................................................................................ 127 4.3.4. Funkcje otrzymujące limity czasowe .............................................................................. 129 4.4. Upraszczanie kodu za pomocą technik synchronizowania operacji ..................................... 131 4.4.1. Programowanie funkcyjne przy użyciu przyszłości ....................................................... 131 4.4.2. Synchronizacja operacji za pomocą przesyłania komunikatów ..................................... 136 4.4.3. Współbieżność w stylu kontynuacji dzięki użyciu Concurrency TS ............................ 141 4.4.4. Łączenie kontynuacji ze sobą .......................................................................................... 143 4.4.5. Oczekiwanie na więcej niż tylko jedną przyszłość ......................................................... 146 4.4.6. Oczekiwanie za pomocą when_any na pierwszą przyszłość w zbiorze ......................... 148 4.4.7. Zasuwy i bariery w Concurrency TS .............................................................................. 151 4.4.8. Zasuwa typu podstawowego — std::experimental::latch ............................................... 151 4.4.9. Podstawowa bariera — std::experimental::barrier ......................................................... 153 4.4.10. std::experimental::flex_barrier, czyli elastyczniejszy przyjaciel std::experimental:barrier .............................................. 155 4.5. Podsumowanie ............................................................................................................................ 156 Poleć książkęKup książkę Spis treści 7 Rozdział 5. Model pamięci języka C++ i operacje na typach atomowych 157 5.1. Podstawowe elementy modelu pamięci ................................................................................... 158 5.1.1. Obiekty i miejsca w pamięci ............................................................................................ 158 5.1.2. Obiekty, miejsca w pamięci i przetwarzanie współbieżne ............................................ 159 5.1.3. Kolejność modyfikacji ...................................................................................................... 161 5.2. Operacje i typy atomowe języka C++ .................................................................................... 161 5.2.1. Standardowe typy atomowe ............................................................................................ 162 5.2.2. Operacje na typie std::atomic_flag .................................................................................. 165 5.2.3. Operacje na typie std::atomic bool ............................................................................ 167 5.2.4. Operacje na typie std::atomic T* — arytmetyka wskaźników ................................. 170 5.2.5. Operacje na standardowych atomowych typach całkowitoliczbowych ........................ 172 5.2.6. Główny szablon klasy std::atomic ............................................................................. 172 5.2.7. Wolne funkcje dla operacji atomowych .......................................................................... 174 5.3. Synchronizacja operacji i wymuszanie ich porządku ............................................................ 176 5.3.1. Relacja synchronizacji ...................................................................................................... 178 5.3.2. Relacja poprzedzania ....................................................................................................... 179 5.3.3. Porządkowanie pamięci na potrzeby operacji atomowych ............................................ 181 5.3.4. Sekwencje zwalniania i relacja synchronizacji ............................................................... 201 5.3.5. Ogrodzenia ....................................................................................................................... 204 5.3.6. Porządkowanie operacji nieatomowych za pomocą operacji atomowych ..................... 206 5.3.7. Porządkowanie operacji nieatomowych .......................................................................... 207 5.4. Podsumowanie ............................................................................................................................ 210 211 6.1. Co oznacza projektowanie struktur danych pod kątem współbieżności? ............................ 212 6.1.1. Wskazówki dotyczące projektowania współbieżnych struktur danych ........................ 213 6.2. Projektowanie współbieżnych struktur danych przy użyciu blokad .................................... 214 6.2.1. Stos gwarantujący bezpieczeństwo przetwarzania wielowątkowego przy użyciu blokad ........................................................................................................... 215 6.2.2. Kolejka gwarantująca bezpieczeństwo przetwarzania wielowątkowego Rozdział 6. Projektowanie współbieżnych struktur danych przy użyciu blokad przy użyciu blokad i zmiennych warunkowych ............................................................. 218 6.2.3. Kolejka gwarantująca bezpieczeństwo przetwarzania wielowątkowego przy użyciu szczegółowych blokad i zmiennych warunkowych .................................... 222 6.3. Projektowanie złożonych struktur danych przy użyciu blokad ............................................. 235 6.3.1. Implementacja tablicy wyszukiwania gwarantującej bezpieczeństwo przetwarzania wielowątkowego przy użyciu blokad ...................................................... 235 6.3.2. Implementacja listy gwarantującej bezpieczeństwo Rozdział 7. Projektowanie współbieżnych struktur danych bez blokad przetwarzania wielowątkowego przy użyciu blokad ...................................................... 241 6.4. Podsumowanie ............................................................................................................................ 246 247 7.1. Definicje i ich praktyczne znaczenie ....................................................................................... 248 7.1.1. Rodzaje nieblokujących struktur danych ........................................................................ 248 7.1.2. Struktury danych bez blokad ........................................................................................... 249 7.1.3. Struktury danych bez oczekiwania ................................................................................. 250 7.1.4. Zalety i wady struktur danych bez blokad ...................................................................... 250 Poleć książkęKup książkę 8 Spis treści 7.2. Przykłady struktur danych bez blokad .................................................................................... 252 7.2.1. Implementacja stosu gwarantującego bezpieczeństwo przetwarzania wielowątkowego bez blokad .................................................................... 253 Rozdział 8. Projektowanie współbieżnego kodu 7.2.2. Eliminowanie niebezpiecznych wycieków — zarządzanie pamięcią w strukturach danych bez blokad .......................................... 257 7.2.3. Wykrywanie węzłów, których nie można odzyskać, za pomocą wskaźników ryzyka ... 262 7.2.4. Wykrywanie używanych węzłów metodą zliczania referencji ...................................... 271 7.2.5. Zmiana modelu pamięci używanego przez operacje na stosie bez blokad ................... 277 7.2.6. Implementacja kolejki gwarantującej bezpieczeństwo przetwarzania wielowątkowego bez blokad .................................................................... 282 7.3. Wskazówki dotyczące pisania struktur danych bez blokad ................................................... 295 7.3.1. Wskazówka: na etapie tworzenia prototypu należy stosować tryb std::memory_order_seq_cst ............................................................................................ 295 7.3.2. Wskazówka: należy używać schematu odzyskiwania pamięci bez blokad .................... 296 7.3.3 Wskazówka: należy unikać problemu ABA .................................................................... 296 7.3.4. Wskazówka: należy identyfikować pętle aktywnego oczekiwania i wykorzystywać czas bezczynności na wspieranie innego wątku ................................. 297 7.4. Podsumowanie ............................................................................................................................ 298 299 8.1. Techniki dzielenia pracy pomiędzy wątki ............................................................................... 300 8.1.1. Dzielenie danych pomiędzy wątki przed rozpoczęciem przetwarzania ....................... 301 8.1.2. Rekurencyjne dzielenie danych ...................................................................................... 302 8.1.3. Dzielenie pracy według typu zadania ............................................................................. 307 8.2. Czynniki wpływające na wydajność współbieżnego kodu ..................................................... 310 8.2.1. Liczba procesorów ........................................................................................................... 310 8.2.2. Współzawodnictwo o dane i ping-pong bufora .............................................................. 311 8.2.3. Fałszywe współdzielenie ................................................................................................. 314 8.2.4. Jak blisko należy rozmieścić dane? ................................................................................. 315 8.2.5. Nadsubskrypcja i zbyt intensywne przełączanie zadań ................................................. 316 8.3. Projektowanie struktur danych pod kątem wydajności przetwarzania wielowątkowego .. 317 8.3.1. Podział elementów tablicy na potrzeby złożonych operacji .......................................... 317 8.3.2. Wzorce dostępu do danych w pozostałych strukturach ................................................. 319 8.4. Dodatkowe aspekty projektowania współbieżnych struktur danych ................................... 321 8.4.1. Bezpieczeństwo wyjątków w algorytmach równoległych .............................................. 321 8.4.2. Skalowalność i prawo Amdahla ....................................................................................... 329 8.4.3. Ukrywanie opóźnień za pomocą wielu wątków .............................................................. 330 8.4.4. Skracanie czasu reakcji za pomocą technik przetwarzania równoległego .................... 332 8.5. Projektowanie współbieżnego kodu w praktyce ..................................................................... 334 8.5.1. Równoległa implementacja funkcji std::for_each ........................................................... 334 8.5.2. Równoległa implementacja funkcji std::find .................................................................. 337 8.5.3. Równoległa implementacja funkcji std::partial_sum ..................................................... 343 8.6. Podsumowanie ............................................................................................................................ 353 355 9.1. Pule wątków ................................................................................................................................ 356 9.1.1. Najprostsza możliwa pula wątków .................................................................................. 356 9.1.2. Oczekiwanie na zadania wysyłane do puli wątków ........................................................ 359 9.1.3. Zadania oczekujące na inne zadania ............................................................................... 363 9.1.4. Unikanie współzawodnictwa w dostępie do kolejki zadań ............................................ 366 9.1.5. Wykradanie zadań ............................................................................................................ 368 Rozdział 9. Zaawansowane zarządzanie wątkami Poleć książkęKup książkę Spis treści 9 Rozdział 10. Algorytmy równoległości 9.2. Przerywanie wykonywania wątków .......................................................................................... 372 9.2.1. Uruchamianie i przerywanie innego wątku .................................................................... 373 9.2.2. Wykrywanie przerwania wątku ....................................................................................... 375 9.2.3. Przerywanie oczekiwania na zmienną warunkową ........................................................ 375 9.2.4. Przerywanie oczekiwania na zmienną typu std::condition_variable_any ..................... 379 9.2.5. Przerywanie pozostałych wywołań blokujących ............................................................ 381 9.2.6. Obsługa przerwań ............................................................................................................ 382 9.2.7. Przerywanie zadań wykonywanych w tle podczas zamykania aplikacji ........................ 383 9.3. Podsumowanie ............................................................................................................................ 384 385 10.1. Algorytmy równoległe w bibliotece standardowej ................................................................. 385 10.2. Polityki wykonywania ................................................................................................................ 386 10.2.1. Ogólny efekt wyboru polityki wykonywania ................................................................. 386 10.2.2. std::execution::sequenced_policy ................................................................................... 388 10.2.3. std::execution::parallel_policy ........................................................................................ 388 10.2.4. std::execution::parallel_unsequenced_policy ................................................................ 389 10.3. Algorytmy równoległości w bibliotece standardowej C++ .................................................. 390 10.3.1. Przykłady używania algorytmów równoległych ............................................................ 392 10.3.2. Licznik odwiedzin ........................................................................................................... 394 10.4. Podsumowanie ............................................................................................................................ 396 397 11.1. Rodzaje błędów związanych z przetwarzaniem współbieżnym ............................................ 398 11.1.1. Niechciane blokowanie ................................................................................................... 398 11.1.2. Sytuacje wyścigu ............................................................................................................. 399 11.2. Techniki lokalizacji błędów związanych z przetwarzaniem współbieżnym ........................ 400 11.2.1. Przeglądanie kodu w celu znalezienia ewentualnych błędów ...................................... 401 11.2.2. Znajdowanie błędów związanych z przetwarzaniem współbieżnym Rozdział 11. Testowanie i debugowanie aplikacji wielowątkowych Dodatek A. Krótki przegląd wybranych elementów języka C++11 poprzez testowanie kodu ................................................................................................. 403 11.2.3. Projektowanie kodu pod kątem łatwości testowania ..................................................... 405 11.2.4. Techniki testowania wielowątkowego kodu .................................................................. 407 11.2.5. Projektowanie struktury wielowątkowego kodu testowego .......................................... 410 11.2.6. Testowanie wydajności wielowątkowego kodu ............................................................. 413 11.3. Podsumowanie ............................................................................................................................ 414 415 A.1. Referencje do r-wartości ........................................................................................................... 415 A.2. Usunięte funkcje ......................................................................................................................... 420 A.3. Funkcje domyślne ...................................................................................................................... 421 A.4. Funkcje constexpr ...................................................................................................................... 425 A.5. Funkcje lambda .......................................................................................................................... 430 A.6. Szablony zmiennoargumentowe ............................................................................................... 436 A.7. Automatyczne określanie typu zmiennej ................................................................................. 440 A.8. Zmienne lokalne wątków ........................................................................................................... 441 A.9. Ustalanie argumentu szablonu klasy ........................................................................................ 442 A.10. Podsumowanie ............................................................................................................................ 443 Poleć książkęKup książkę 10 Spis treści Dodatek B. Krótkie zestawienie bibliotek przetwarzania współbieżnego Dodatek C. Framework przekazywania komunikatów i kompletny przykład implementacji systemu bankomatu 445 447 Dodatek D. Biblioteka wątków języka C++ 465 D.1. Nagłówek chrono ................................................................................................................. 465 D.2. Nagłówek condition_variable ............................................................................................ 481 D.3. Nagłówek atomic ................................................................................................................. 498 D.4. Nagłówek future .................................................................................................................. 536 D.5. Nagłówek mutex .................................................................................................................. 561 D.6. Nagłówek ratio ..................................................................................................................... 613 D.7. Nagłówek thread ................................................................................................................. 619 Skorowidz 631 Poleć książkęKup książkę Synchronizacja współbieżnych operacji W tym rozdziale zostaną omówione następujące zagadnienia:  oczekiwanie na zdarzenie;  oczekiwanie na jednorazowe zdarzenia za pomocą przyszłości;  oczekiwanie z limitem czasowym;  upraszczanie kodu za pomocą technik synchronizowania operacji. W poprzednim rozdziale przeanalizowaliśmy rozmaite sposoby ochrony danych współ- dzielonych przez wiele wątków. Okazuje się jednak, że w pewnych przypadkach jest potrzebna nie tyle ochrona danych, co synchronizacja działań podejmowanych przez różne wątki. Wątek może na przykład czekać z realizacją własnej operacji na zakoń- czenie pewnego zadania przez inny wątek. Ogólnie w wielu przypadkach wątek oczeku- jący na określone zdarzenie lub spełnienie pewnego warunku jest najwygodniejszym rozwiązaniem. Mimo że analogiczne rozwiązanie można zaimplementować w formie mechanizmu okresowego sprawdzania flagi zakończonego zadania lub innej wartości zapisanej we współdzielonych danych, taki model byłby daleki od ideału. Konieczność synchronizacji operacji wykonywanych przez różne wątki jest dość typowym scena- riuszem, zatem biblioteka standardowa języka C++ oferuje mechanizmy ułatwiające obsługę tego modelu, w tym zmienne warunkowe i tzw. przyszłości. Te funkcje zo- stały rozszerzone w specyfikacji technicznej współbieżności i dostarczają dodatkowe operacje dla przyszłości oraz nowe możliwości w postaci zasuw i barier. Poleć książkęKup książkę 102 ROZDZIAŁ 4. Synchronizacja współbieżnych operacji W tym rozdziale omówię techniki oczekiwania na zdarzenia przy użyciu zmiennych warunkowych, przyszłości, zasuw i barier, oraz sposoby upraszczania synchronizacji operacji. 4.1. Oczekiwanie na zdarzenie lub inny warunek Przypuśćmy, że podróżujemy nocnym pociągiem. Jednym ze sposobów zagwaranto- wania, że wysiądziemy na właściwej stacji, jest unikanie snu i sprawdzanie wszystkich stacji, na których zatrzymuje się nasz pociąg. W ten sposób nie przegapimy naszej stacji, jednak po dotarciu na miejsce będziemy bardzo zmęczeni. Alternatywnym rozwiąza- niem jest sprawdzenie rozkładu jazdy pod kątem godziny przyjazdu, ustawienie budzika z pewnym wyprzedzeniem względem tej godziny i pójście spać. To rozwiązanie jest dość bezpieczne — nie przegapimy naszej stacji, ale jeśli pociąg się spóźni, wstaniemy zbyt wcześnie. Nie można też wykluczyć sytuacji, w której wyczerpią się baterie w budziku — w takim przypadku możemy zaspać i przegapić swoją stację. Idealnym rozwiązaniem byłaby możliwość pójścia spać i skorzystania z pomocy czegoś (lub kogoś), co obudziłoby nas bezpośrednio przed osiągnięciem stacji docelowej. Jaki to ma związek z wątkami? Jeśli jeden wątek czeka, aż inny wątek zakończy jakieś zadanie, ma do wyboru kilka możliwych rozwiązań. Po pierwsze, może stale spraw- dzać odpowiednią flagę we współdzielonych danych (chronionych przez muteks); flaga zostanie ustawiona przez drugi wątek w momencie zakończenia zadania. Takie rozwią- zanie jest nieefektywne z dwóch powodów: wątek, który wielokrotnie sprawdza wspo- mnianą flagę, zajmuje cenny czas procesora, a muteks zablokowany przez oczekujący wątek nie jest dostępny dla żadnego innego wątku. Oba te czynniki działają na nieko- rzyść oczekującego wątku, ponieważ ten wątek zajmuje zasoby potrzebne także do działania wątku, na który czeka, co opóźnia wykonanie zadania i ustawienie odpowied- niej flagi. Sytuacja przypomina unikanie snu przez całą podróż pociągiem i prowadze- nie rozmowy z maszynistą — maszynista zajęty rozmową musi prowadzić pociąg nieco wolniej, zatem później dotrzemy na swoją stację. Podobnie wątek oczekujący zajmuje zasoby, które mogłyby być używane przez pozostałe wątki w systemie, przez co czas oczekiwania może być dłuższy, niż to konieczne. Druga opcja polega na przechodzeniu wątku oczekującego w stan uśpienia na krótkie momenty i okresowym wykonywaniu testów za pomocą funkcji std::this_ thread::sleep_for() (patrz punkt 4.3): bool flag; std::mutex m; void wait_for_flag() { std::unique_lock std::mutex lk(m); while(!flag) { lk.unlock(); (cid:8286) Odblokowuje muteks std::this_thread::sleep_for(std::chrono::milliseconds(100)); (cid:8287) Czeka 100 ms lk.lock(); (cid:8288) Ponownie blokuje muteks } } Poleć książkęKup książkę 4.1. Oczekiwanie na zdarzenie lub inny warunek 103 Wywołanie funkcji w pętli odblokowuje muteks (cid:8286) przed przejściem w stan uśpienia (cid:8287) i ponownie blokuje ten muteks po wyjściu z tego stanu (cid:8288) — dzięki temu drugi wątek ma szanse uzyskania dostępu do flagi i jej ustawienia. Opisane rozwiązanie jest o tyle dobre, że uśpiony wątek nie zajmuje bezproduk- tywnie czasu procesora. Warto jednak pamiętać, że dobór właściwego czasu uśpienia jest dość trudny. Zbyt krótki czas przebywania w tym stanie spowoduje, że wątek będzie tracił czas procesora na zbyt częste testy; zbyt długi czas uśpienia będzie oznaczał, że wątek będzie przebywał w tym stanie nawet po zakończeniu zadania, na które oczekuje, zatem opóźnienie w działaniu wątku oczekującego będzie zbyt duże. Takie „zaspanie” wątku rzadko ma bezpośredni wpływ na wynik operacji wykonywanych przez program, ale już w przypadku szybkiej gry może powodować pominięcie niektórych klatek animacji, a w przypadku aplikacji czasu rzeczywistego może oznaczać pominięcie przydziału czasu procesora. Trzecim, najlepszym rozwiązaniem jest użycie gotowych elementów biblioteki stan- dardowej języka C++ umożliwiających oczekiwanie na określone zdarzenie. Najprost- szym mechanizmem oczekiwania na zdarzenie generowane przez inny wątek (na przy- kład zdarzenie polegające na umieszczeniu dodatkowego zadania w potoku) jest tzw. zmienna warunkowa. Zmienna warunkowa jest powiązana z pewnym zdarzeniem lub warunkiem oraz co najmniej jednym wątkiem, który czeka na spełnienie tego warunku. Wątek, który odkrywa, że warunek jest spełniony, może powiadomić pozostałe wątki oczekujące na tę zmienną warunkową, aby je obudzić i umożliwić im dalsze przetwarzanie. 4.1.1. Oczekiwanie na spełnienie warunku za pomocą zmiennych warunkowych Biblioteka standardowa języka C++ udostępnia dwie implementacje mechanizmu zmiennych warunkowych w formie klas std::condition_variable i std::condition_ variable_any. Obie klasy zostały zadeklarowane w pliku nagłówkowym condition_ variable . W obu przypadkach zapewnienie właściwej synchronizacji wymaga uży- cia muteksu — pierwsza klasa jest przystosowana tylko do obsługi muteksów typu std::mutex, natomiast druga klasa obsługuje wszystkie rodzaje muteksów spełniających pewien minimalny zbiór kryteriów (stąd przyrostek _any). Ponieważ klasa std::condition_ variable_any jest bardziej uniwersalna, z jej stosowaniem wiążą się dodatkowe koszty w wymiarze wielkości, wydajności i zasobów systemu operacyjnego. Jeśli więc nie potrzebujemy dodatkowej elastyczności, powinniśmy stosować klasę std::condition_ variable. Jak należałoby użyć klasy std::condition_variable do obsługi przykładu opisanego na początku tego podrozdziału — jak sprawić, że wątek oczekujący na wykonanie jakie- goś zadania będzie uśpiony do momentu, w którym będą dostępne dane do przetwo- rzenia? Na listingu 4.1 pokazano przykład kodu implementującego odpowiednie roz- wiązanie przy użyciu zmiennej warunkowej. Listing 4.1. Oczekiwanie na dane do przetworzenia za pomocą klasy std::condition_variable std::mutex mut; std::queue data_chunk data_queue; (cid:8286) std::condition_variable data_cond; Poleć książkęKup książkę 104 ROZDZIAŁ 4. Synchronizacja współbieżnych operacji void data_preparation_thread() { while(more_data_to_prepare()) { data_chunk const data=prepare_data(); std::lock_guard std::mutex lk(mut); data_queue.push(data); (cid:8287) data_cond.notify_one(); (cid:8288) } } void data_processing_thread() { while(true) { std::unique_lock std::mutex lk(mut); (cid:8289) data_cond.wait( lk,[]{return !data_queue.empty();}); (cid:8290) data_chunk data=data_queue.front(); data_queue.pop(); lk.unlock(); (cid:8291) process(data); if(is_last_chunk(data)) break; } } Na początku kodu zdefiniowano kolejkę (cid:8286), która będzie używana do przekazywania danych pomiędzy dwoma wątkami. Kiedy dane są gotowe do przetworzenia, wątek, który je przygotował, blokuje muteks chroniący kolejkę za pomocą klasy std::lock_ guard i umieszcza nowe dane w kolejce (cid:8287). Wątek wywołuje następnie funkcję skła- dową notify_one() dla obiektu klasy std::condition_variable, aby powiadomić ocze- kujący wątek (jeśli taki istnieje) o dostępności nowych danych (cid:8288). Zwróć uwagę na umieszczenie w mniejszym zasięgu kodu odpowiedzialnego z przekazywanie danych do kolejki. Zmienna warunkowa jest definiowana po odblokowaniu muteksu — jeżeli wątek oczekujący zostanie obudzony natychmiast, nie będzie musiał ponownie nakła- dać blokady w oczekiwaniu na odblokowanie muteksu. W tym modelu drugą stroną komunikacji jest wątek przetwarzający te dane. Wątek przetwarzający najpierw blokuje muteks, jednak tym razem użyto do tego celu klasy std::unique_lock zamiast klasy std::lock_guard (cid:8289) — przyczyny tej decyzji zostaną wyjaśnione za chwilę. Wątek wywołuje następnie funkcję wait() dla obiektu klasy std::condition_variable. Na wejściu tego wywołania wątek przekazuje obiekt blokady i funkcję lambda reprezentującą warunek, który musi zostać spełniony przed przystą- pieniem do dalszego przetwarzania (cid:8290). Funkcje lambda to stosunkowo nowy element (wprowadzony w standardzie C++11), który umożliwia pisanie funkcji anonimowych w ramach innych wyrażeń. Wspomniane rozwiązanie wprost idealnie nadaje się do wskazywania predykatów w wywołaniach takich funkcji biblioteki standardowej jak wait(). W tym przypadku prosta funkcja lambda []{return !data_queue.empty();} sprawdza, czy struktura reprezentowana przez zmienną data_queue nie jest pusta, tj. Poleć książkęKup książkę 4.1. Oczekiwanie na zdarzenie lub inny warunek 105 czy kolejka zawiera jakieś dane gotowe do przetworzenia. Funkcje lambda zostaną szcze- gółowo omówione w części A.5 dodatku A. Implementacja funkcji wait() sprawdza warunek (wywołując przekazaną funkcję lambda), po czym zwraca sterowanie, jeśli ten warunek jest spełniony (jeśli funkcja lambda zwróciła wartość true). Jeśli warunek nie jest spełniony (jeśli funkcja lambda zwróciła wartość false), funkcja wait() odblokowuje muteks i wprowadza bieżący wątek w stan blokady (oczekiwania). Kiedy zmienna warunkowa jest powiadamiana za pomocą funkcji notify_one() wywołanej przez wątek przygotowujący dane, wątek oczekujący jest budzony (odblokowywany), ponownie uzyskuje blokadę muteksu i jeszcze raz sprawdza warunek. Jeśli warunek dalszego przetwarzania jest spełniony, funkcja wait() zwraca sterowanie z zachowaniem blokady muteksu. Jeśli warunek nie jest spełniony, wątek odblokowuje muteks i ponownie przechodzi w stan oczekiwania. Właśnie dlatego w przy- kładzie należało użyć klasy std::unique_lock zamiast klasy std::lock_guard — wątek oczekujący musi odblokować muteks na czas oczekiwania i zablokować go ponownie po otrzymaniu powiadomienia, a klasa std::lock_guard nie zapewnia takiej elastycz- ności. Gdyby muteks pozostał zablokowany przez cały czas uśpienia tego wątku, wątek przygotowujący dane nie mógłby zablokować tego muteksu i dodać elementu do kolejki, zatem warunek budzenia wątku oczekującego nigdy nie zostałby spełniony. Na listingu 4.1 użyłem prostej funkcji lambda (cid:8290), która sprawdza, czy struktura kolejki nie jest pusta. Okazuje się, że w tej roli równie dobrze można by użyć dowolnej funkcji lub obiektu wywoływalnego. Jeśli programista dysponuje już funkcją spraw- dzającą odpowiedni warunek (funkcja może oczywiście być nieporównanie bardziej złożona niż prosty test z powyższego przykładu), może przekazać tę funkcję bezpo- średnio na wejściu funkcji wait(), bez konieczności opakowywania jej w ramach wyra- żenia lambda. Po wywołaniu funkcji wait() zmienna warunkowa może sprawdzić wskazany warunek na wiele różnych sposobów, jednak podczas tego testu muteks zawsze jest zablokowany, a funkcja wait() natychmiast zwraca sterowanie, pod warun- kiem że przekazana funkcja sprawdzająca ten warunek zwróciła wartość true. Jeśli wątek oczekujący ponownie uzyskuje muteks i sprawdza warunek, mimo że nie otrzy- mał powiadomienia od innego wątku i jego działania nie są bezpośrednią odpowiedzią na takie powiadomienie, mamy do czynienia z tzw. pozornym budzeniem (ang. spu- rious wake). Ponieważ optymalna liczba i częstotliwość takich pozornych budzeń są z definicji trudne do oszacowania, funkcja sprawdzająca prawdziwość warunku nie powinna powodować żadnych skutków ubocznych. Gdyby ta funkcja powodowała skutki uboczne, programista musiałby przygotować swój kod na wielokrotne występowanie tych skutków przed spełnieniem warunku. Ogólnie rzecz biorąc, std::condition_variable::wait to rodzaj optymalizacji. Istotnie tak jest: odpowiednio przygotowana (choć nieidealna) technika implementacji to po prostu zwykła pętla. template typename Predicate void minimal_wait(std::unique_lock std::mutex lk,Predicate pred){ while(!pred()){ lk.unlock(); lk.lock(); } } Poleć książkęKup książkę 106 ROZDZIAŁ 4. Synchronizacja współbieżnych operacji Kod musi być przygotowany do pracy z tak minimalną implementacją funkcji wa- it(), a także implementacją, która budzi wątek jedynie po wywołaniu notify_one() lub notify_all(). Możliwość odblokowania obiektu klasy std::unique_lock nie jest używana tylko dla wywołania funkcji wait() — analogiczne rozwiązanie zastosowaliśmy po uzyskaniu danych do przetworzenia, ale przed przystąpieniem do właściwego przetwarzania (cid:8291). Przetwarzanie danych może być czasochłonną operacją, a jak wiemy z rozdziału 3., utrzymywanie blokady muteksu dłużej, niż to konieczne, nie jest dobrym rozwiązaniem. Stosowanie struktury kolejki do przekazywania danych pomiędzy wątkami (jak na listingu 4.1) jest dość typowym rozwiązaniem. Jeśli projekt aplikacji jest właściwy, synchronizacja powinna dotyczyć samej kolejki, co znacznie ogranicza liczbę poten- cjalnych problemów i problematycznych sytuacji wyścigu. Spróbujmy więc wyodręb- nić z listingu 4.1 uniwersalną kolejkę gwarantującą bezpieczne przetwarzanie wielo- wątkowe. 4.1.2. Budowa kolejki gwarantującej bezpieczne przetwarzanie wielowątkowe przy użyciu zmiennych warunkowych Przed przystąpieniem do projektowania uniwersalnej kolejki warto poświęcić kilka minut analizie operacji, które trzeba będzie zaimplementować dla tej struktury danych (podobnie jak w przypadku stosu gwarantującego bezpieczeństwo przetwarzania wielo- wątkowego z punktu 3.2.3). Przyjrzyjmy się kontenerowi std::queue dostępnemu w bibliotece standardowej języka C++ (patrz listing 4.2), który będzie stanowił punkt wyjścia dla naszej implementacji. Listing 4.2. Interfejs kontenera std::queue template class T, class Container = std::deque T class queue { public: explicit queue(const Container ); explicit queue(Container = Container()); template class Alloc explicit queue(const Alloc ); template class Alloc queue(const Container , const Alloc ); template class Alloc queue(Container , const Alloc ); template class Alloc queue(queue , const Alloc ); void swap(queue q); bool empty() const; size_type size() const; T front(); const T front() const; T back(); const T back() const; void push(const T x); void push(T x); void pop(); template class... Args void emplace(Args ... args); }; Jeśli pominiemy operacje konstruowania, przypisywania i wymiany, pozostaną nam zaledwie trzy grupy operacji: operacje zwracające stan całej kolejki (empty() i size()), operacje zwracające pojedyncze elementy kolejki (front() i back()) oraz operacje Poleć książkęKup książkę 4.1. Oczekiwanie na zdarzenie lub inny warunek 107 modyfikujące kolejkę (push(), pop() i emplace()). Mamy więc do czynienia z sytuacją analogiczną do tej opisanej w punkcie 3.2.3 (gdzie omawialiśmy strukturę stosu), zatem opisany interfejs jest narażony na te same problemy związane z sytuacjami wyścigów. W tym przypadku należy połączyć funkcje front() i pop() w jedno wywołanie, tak jak wcześniej połączyliśmy funkcje top() i pop() dla struktury stosu. Warto jeszcze zwrócić uwagę na pewien nowy element w kodzie z listingu 4.1 — podczas używania kolejki do przekazywania danych pomiędzy wątkami wątek docelowy zwykle musi czekać na te dane. Warto więc zaimplementować funkcję pop() w dwóch wersjach — pierwsza funkcja, try_pop(), próbuje pobrać wartość z kolejki, ale zawsze zwraca sterowanie bezpośrednio po wywołaniu, nawet jeśli kolejka nie zawierała żadnej wartości (wtedy funkcja sygnalizuje błąd); druga funkcja, wait_and_pop(), czeka na pojawienie się w kolejce wartości do pobrania. Po wprowadzeniu zmian zgodnie ze schematem opi- sanym już przy okazji przykładu stosu interfejs struktury kolejki powinien wyglądać tak jak na listingu 4.3. Listing 4.3. Interfejs struktury danych threadsafe_queue #include memory Dla typu std::shared_ptr template typename T class threadsafe_queue { public: threadsafe_queue(); threadsafe_queue(const threadsafe_queue ); threadsafe_queue operator=( const threadsafe_queue ) = delete; Dla uproszczenia wyklucza możliwość przypisywania void push(T new_value); bool try_pop(T value); (cid:8286) std::shared_ptr T try_pop(); (cid:8287) void wait_and_pop(T value); std::shared_ptr T wait_and_pop(); bool empty() const; }; Podobnie jak w przypadku stosu, na listingu 4.3 usunięto konstruktory i operator przypisania, aby uprościć analizowany kod. Tak jak wcześniej, także tym razem funk- cje try_pop() i wait_for_pop() występują w dwóch wersjach. Pierwsza przeciążona wersja funkcji try_pop() (cid:8286) zapisuje pobraną wartość we wskazywanej zmiennej, tak aby można było użyć tej wartości w roli informacji o stanie; funkcja zwraca wartość true, jeśli uzyskała jakąś wartość — w przeciwnym razie funkcja zwraca wartość false (patrz część A.2 dodatku A). Druga przeciążona wersja (cid:8287) nie może działać w ten sam sposób, ponieważ natychmiast zwraca uzyskaną wartość. Jeśli jednak funkcja nie uzy- skała żadnej wartości, może zwrócić wskaźnik równy NULL. Jaki to ma związek z listingiem 4.1? Okazuje się, że możemy wyodrębnić kod funkcji push() i wait_and_pop() z tamtego listingu i na tej podstawie przygotować nową imple- mentację (patrz listing 4.4). Poleć książkęKup książkę 108 ROZDZIAŁ 4. Synchronizacja współbieżnych operacji Listing 4.4. Funkcje push() i wait_and_pop() wyodrębnione z listingu 4.1 #include queue #include mutex #include condition_variable template typename T class threadsafe_queue { private: std::mutex mut; std::queue T data_queue; std::condition_variable data_cond; public: void push(T new_value) { std::lock_guard std::mutex lk(mut); data_queue.push(new_value); data_cond.notify_one(); } void wait_and_pop(T value) { std::unique_lock std::mutex lk(mut); data_cond.wait(lk,[this]{return !data_queue.empty();}); value=data_queue.front(); data_queue.pop(); } }; threadsafe_queue data_chunk data_queue; (cid:8286) void data_preparation_thread() { while(more_data_to_prepare()) { data_chunk const data=prepare_data(); data_queue.push(data); (cid:8287) } } void data_processing_thread() { while(true) { data_chunk data; data_queue.wait_and_pop(data); (cid:8288) process(data); if(is_last_chunk(data)) break; } } Muteks i zmienna warunkowa są teraz elementami składowymi obiektu klasy threadsafe_ queue, zatem nie jest potrzebne stosowanie odrębnych zmiennych (cid:8286), a wywołanie funkcji push() nie wymaga zewnętrznych mechanizmów synchronizacji (cid:8287). Jak widać, także funkcja wait_and_pop() uwzględnia stan zmiennej warunkowej (cid:8288). Poleć książkęKup książkę 4.1. Oczekiwanie na zdarzenie lub inny warunek 109 Napisanie drugiej wersji przeciążonej funkcji wait_and_pop() nie stanowi żadnego problemu; także pozostałe funkcje można niemal skopiować z przykładu stosu pokaza- nego na listingu 3.5. Ostateczną wersję implementacji kolejki pokazano na listingu 4.5. Listing 4.5. Kompletna definicja klasy kolejki gwarantującej bezpieczeństwo przetwarzania wielowątkowego (dzięki użyciu zmiennych warunkowych) #include queue #include memory #include mutex #include condition_variable template typename T class threadsafe_queue { private: mutable std::mutex mut; (cid:8286) Muteks musi być modyfikowalny std::queue T data_queue; std::condition_variable data_cond; public: threadsafe_queue() {} threadsafe_queue(threadsafe_queue const other) { std::lock_guard std::mutex lk(other.mut); data_queue=other.data_queue; } void push(T new_value) { std::lock_guard std::mutex lk(mut); data_queue.push(new_value); data_cond.notify_one(); } void wait_and_pop(T value) { std::unique_lock std::mutex lk(mut); data_cond.wait(lk,[this]{return !data_queue.empty();}); value=data_queue.front(); data_queue.pop(); } std::shared_ptr T wait_and_pop() { std::unique_lock std::mutex lk(mut); data_cond.wait(lk,[this]{return !data_queue.empty();}); std::shared_ptr T res(std::make_shared T (data_queue.front())); data_queue.pop(); return res; } bool try_pop(T value) { std::lock_guard std::mutex lk(mut); if(data_queue.empty()) return false; Poleć książkęKup książkę 110 ROZDZIAŁ 4. Synchronizacja współbieżnych operacji value=data_queue.front(); data_queue.pop(); return true; } std::shared_ptr T try_pop() { std::lock_guard std::mutex lk(mut); if(data_queue.empty()) return std::shared_ptr T (); std::shared_ptr T res(std::make_shared T (data_queue.front())); data_queue.pop(); return res; } bool empty() const { std::lock_guard std::mutex lk(mut); return data_queue.empty(); } }; Mimo że empty() jest stałą funkcją składową i mimo że parametr other konstruktora kopiującego jest stałą referencją, pozostałe wątki mogą dysponować niestałymi refe- rencjami do tego obiektu i wywoływać funkcje składowe zmieniające jego stan, zatem blokowanie muteksu wciąż jest konieczne. Ponieważ blokowanie muteksu jest operacją zmieniającą stan obiektu, obiekt muteksu należy oznaczyć jako modyfikowalny (ang. mutable) (cid:8286), tak aby można było blokować ten muteks w ciele funkcji empty() i kon- struktora kopiującego. Zmienne warunkowe są przydatne także w sytuacji, w której wiele wątków czeka na to samo zdarzenie. Jeśli celem stosowania wątków jest dzielenie obciążenia i jeśli tylko jeden wątek powinien reagować na powiadomienie, można zastosować dokładnie taką samą strukturę jak ta z listingu 4.1 — wystarczy uruchomić wiele egzemplarzy wątku przetwarzającego dane. Po przygotowaniu nowych danych wywołanie funkcji noti- fy_one() spowoduje, że jeden z wątków aktualnie wykonujących funkcję wait() sprawdzi warunek. Ponieważ do struktury data_queue właśnie dodano nowe dane, funkcja wait() zwróci sterowanie (z powodu dodania elementu do egzemplarza da- ta_queue). Nie wiadomo, do którego wątku trafi powiadomienie ani nawet czy istnieje wątek oczekujący na to powiadomienie (nie można przecież wykluczyć, że wszyst- kie wątki w danej chwili przetwarzają swoje dane). Warto też pamiętać o możliwości oczekiwania na to samo zdarzenie przez wiele wątków, z których każdy musi zareagować na powiadomienie. Opisany scenariusz może mieć związek z inicjalizacją współdzielonych danych, gdzie wszystkie wątki przetwa- rzające operują na tych samych danych i muszą czekać albo na ich inicjalizację (w takim przypadku istnieją lepsze mechanizmy — patrz punkt 3.3.1 w rozdziale 3.), albo na ich aktualizację (na przykład w ramach okresowej, wielokrotnej inicjalizacji). W opisanych przypadkach wątek przygotowujący dane może wywołać funkcję składową notify_all() dla zmiennej warunkowej (zamiast funkcji notify_one()). Jak nietrudno się domyślić, funkcja powoduje, że wszystkie wątki aktualnie wykonujące funkcję wait() sprawdzą warunek, na który czekają. Poleć książkęKup książkę 4.2. Oczekiwanie na jednorazowe zdarzenia za pomocą przyszłości 111 Jeśli wątek wywołujący w założeniu ma oczekiwać na dane zdarzenie tylko raz, czyli jeśli po spełnieniu warunku wątek nie będzie ponownie czekał na tę samą zmienną warunkową, być może warto zastosować inny mechanizm synchronizacji niż zmienna warunkowa. Zmienne warunkowe są szczególnie nieefektywne w sytuacji, gdy warun- kiem, na który oczekują wątki, jest dostępność określonego elementu danych. W takim przypadku lepszym rozwiązaniem jest użycie mechanizmu przyszłości. 4.2. Oczekiwanie na jednorazowe zdarzenia za pomocą przyszłości Przypuśćmy, że planujemy podróż samolotem. Po przyjeździe na lotnisko i przejściu rozmaitych procedur wciąż musimy czekać na komunikat dotyczący gotowości naszego samolotu na przyjęcie pasażerów (zdarza się, że pasażerowie muszą czekać wiele godzin). Możemy oczywiście znaleźć sposób, aby ten czas minął nieco szybciej (możemy na przykład czytać książkę, przeglądać strony internetowe lub udać się na posiłek do drogiej lotniskowej kawiarni), jednak niezależnie od sposobu spędzania czasu czekamy na jedno — sygnał wzywający do udania się na pokład samolotu. Co więcej, interesu- jący nas lot odbędzie się tylko raz, zatem przy okazji następnego wyjazdu na wakacje będziemy czekali na inny lot. Twórcy biblioteki standardowej języka C++ rozwiązali problem jednorazowych zdarzeń za pomocą mechanizmu nazwanego przyszłością (ang. future). Wątek, który musi czekać na określone jednorazowe zdarzenie, powinien uzyskać przyszłość repre- zentującą to zdarzenie. Wątek oczekujący na tę przyszłość może następnie okresowo sprawdzać, czy odpowiednie zdarzenie nie nastąpiło (tak jak pasażerowie co jakiś czas zerkają na tablicę odlotów), i jednocześnie pomiędzy tymi testami wykonywać inne zadanie (spożywać drogi deser w lotniskowej kawiarni). Alternatywnym rozwiązaniem jest wykonywanie innego zadania do momentu, w którym dalsze działanie nie jest moż- liwe bez określonego zdarzenia, i przejście w stan gotowości na przyszłość. Przyszłość może, ale nie musi być powiązana z danymi (tak jak tablica odlotów może wskazywać rękawy prowadzące do właściwych samolotów). Po wystąpieniu zdarzenia (po osiągnię- ciu gotowości przez przyszłość) nie jest możliwe wyzerowanie tej przyszłości. W bibliotece standardowej języka C++ istnieją dwa rodzaje przyszłości zaimple- mentowane w formie dwóch szablonów klas zadeklarowanych w nagłówku biblioteki future : przyszłości unikatowe (std::future ) oraz przyszłości współdzielone (std:: shared_future ). Wymienione klasy opracowano na bazie typów std::unique_ptr i std::shared_ptr. Obiekt typu std::future jest jedynym egzemplarzem odwołującym się do powiązanego zdarzenia, natomiast do jednego zdarzenia może się odwoływać wiele egzemplarzy typu std::shared_future. W drugim przypadku wszystkie egzem- plarze są gotowe jednocześnie i wszystkie mogą uzyskiwać dostęp do dowolnych danych powiązanych z danym zdarzeniem. Właśnie z myślą o powiązanych danych zaprojektowa- no te szablony klas — tak jak w przypadku szablonów std::unique_ptr i std::shared_ptr, parametry szablonów std::future i std::shared_future reprezentują właśnie typy powiązanych danych. W razie braku powiązanych danych należy stosować następujące specjalizacje tych szablonów: std:future void i std::shared_future void . Mimo że przyszłości służą do komunikacji pomiędzy wątkami, same obiekty przyszłości nie oferują mechanizmów synchronizowanego dostępu. Jeśli wiele wątków potrzebuje dostępu do Poleć książkęKup książkę 112 ROZDZIAŁ 4. Synchronizacja współbieżnych operacji jednego obiektu przyszłości, należy chronić ten dostęp za pomocą muteksu lub innego mechanizmu synchronizacji (patrz rozdział 3.). Jak napiszę w punkcie 4.2.5 w dalszej części tego podrozdziału, wiele wątków może uzyskiwać dostęp do własnej kopii obiektu typu std::shared_future bez konieczności dodatkowej synchronizacji, nawet jeśli wszystkie te kopie odwołują się do tego samego asynchronicznego wyniku. Specyfikacja techniczna Concurrency TS dostarcza rozszerzane wersje tych sza- blonów klas w przestrzeni nazw std::experimental — std::experimental::future i std::experimental::shared_future . Działają one analogicznie do ich odpowiedników w przestrzeni nazw std, przy czym oferują dodatkowe funkcje składowe dostarczające kolejne możliwości. Trzeba koniecznie zwrócić uwagę na to, że przestrzeń nazw std:: experimental nie narzuca żadnych wymagań w zakresie jakości kodu (mam nadzieję, że implementacja będzie miała taką samą jakość jak pozostały kod znajdujący się w biblio- tece danego dostawcy). Trzeba podkreślić fakt, że to nie są standardowe klasy i funkcje, więc mogą nie mieć dokładnie tej samej składni i semantyki, gdy wreszcie zostaną za- adaptowane (o ile w ogóle tak się stanie) w przyszłych wersjach standardu C++. Aby móc skorzystać z możliwości oferowanych przez te klasy i funkcje, konieczne jest umieszczenie w kodzie polecenia dodającego nagłówek experimental/future . Najprostszym przykładem jednorazowego zdarzenia jest wynik obliczeń wykonywa- nych w tle. Już w rozdziale 2. napisałem, że klasa std::thread nie udostępnia prostych mechanizmów zwracania wartości wynikowych dla tego rodzaju zadań, i zapowiedzia- łem wprowadzenie odpowiednich rozwiązań w rozdziale 4. przy okazji omawiania przy- szłości — czas zapoznać się z tymi rozwiązaniami. 4.2.1. Zwracanie wartości przez zadania wykonywane w tle Przypuśćmy, że nasza aplikacja wykonuje czasochłonne obliczenia, które ostatecznie pozwolą uzyskać oczekiwany wynik. Załóżmy, że wartość wynikowa nie jest potrzebna na tym etapie działania programu. Być może udało nam się wymyślić sposób poszu- kiwania odpowiedzi na pytanie o życie, wszechświat i całą resztę stawiane w książkach Douglasa Adamsa1. Moglibyśmy oczywiście uruchomić nowy wątek, który wykona niezbędne obliczenia, jednak takie rozwiązanie wiązałoby się z koniecznością przeka- zania wyników z powrotem do wątku głównego, ponieważ klasa std::thread nie oferuje alternatywnego mechanizmu zwracania wartości wynikowych. W takim przypadku spo- rym ułatwieniem jest użycie szablonu funkcji std::async (zadeklarowanego w pliku na- główkowym future ). Asynchroniczne zadanie, którego wynik nie jest potrzebny na bieżącym etapie dzia- łania programu, można rozpocząć za pomocą funkcji std::async. Zamiast zwracania obiektu klasy std::thread, który umożliwi oczekiwanie na zakończenie danego wątku, funkcja std::async zwraca obiekt klasy std::future, który w przyszłości będzie zawierał wartość wynikową. W miejscu, w którym aplikacja będzie potrzebowała tej wartości, należy wywołać funkcję get() dla obiektu przyszłości — wywołanie tej funkcji zablo- kuje wykonywanie bieżącego wątku do momentu osiągnięcia gotowości przez przy- szłość, po czym zwróci uzyskaną wartość. Prosty przykład użycia tych elementów poka- zano na listingu 4.6. 1 W książce Autostopem przez Galaktykę zbudowano komputer Deep Thought, który miał odpowiedzieć na pytanie o życie, wszechświat i całą resztę. Odpowiedzią na to pytanie była liczba 42. Poleć książkęKup książkę 4.2. Oczekiwanie na jednorazowe zdarzenia za pomocą przyszłości 113 Listing 4.6. Przykład użycia szablonu klasy std::future do uzyskania wartości wynikowej asynchronicznego zadania #include future #include iostream int find_the_answer_to_ltuae(); void do_other_stuff(); int main() { std::future int the_answer=std::async(find_the_answer_to_ltuae); do_other_stuff(); std::cout Odpowiedź brzmi the_answer.get() std::endl; } Szablon funkcji std::async umożliwia przekazywanie dodatkowych argumentów na wejściu wywoływanej funkcji — wystarczy dodać te argumenty do wywołania (podob- nie jak w przypadku klasy std::thread). Jeśli pierwszy argument reprezentuje wskaźnik do funkcji składowej, drugi argument zawiera obiekt, dla którego ma zostać wywołana ta funkcja składowa (bezpośrednio, za pośrednictwem wskaźnika lub poprzez opako- wanie std::ref), a pozostałe argumenty są przekazywane na wejściu tej funkcji składo- wej. W przeciwnym razie drugi i kolejne argumenty są przekazywane na wejściu funkcji składowej lub wywoływalnego obiektu wskazanego za pośrednictwem pierwszego argu- mentu. Tak jak w przypadku klasy std::thread, jeśli argumenty mają postać r-wartości, zostaną utworzone kopie poprzez przeniesienie oryginalnych wartości. Dzięki temu możemy stosować typy oferujące tylko możliwość przenoszenia zarówno w roli obiektów funkcji, jak i w roli argumentów. Przykład takiego rozwiązania pokazano na listingu 4.7. Listing 4.7. Przekazywanie argumentów na wejściu funkcji wątku std::async #include string #include future Wywołuje p- foo(42, witaj ), gdzie p jest reprezentowane przez x Wywołuje tmpx.bar( żegnaj ), gdzie tmpx jest kopią x struct X { void foo(int,std::string const ); std::string bar(std::string const ); }; X x; auto f1=std::async( X::foo, x,42, witaj ); auto f2=std::async( X::bar,x, żegnaj ); struct Y { double operator()(double); }; Y y; auto f3=std::async(Y(),3.141); auto f4=std::async(std::ref(y),2.718); Wywołuje y(2.718) X baz(X ); std::async(baz,std::ref(x)); Wywołuje baz(x) class move_only {
Pobierz darmowy fragment (pdf)

Gdzie kupić całą publikację:

Język C++ i przetwarzanie współbieżne w akcji. Wydanie II
Autor:

Opinie na temat publikacji:


Inne popularne pozycje z tej kategorii:


Czytaj również:


Prowadzisz stronę lub blog? Wstaw link do fragmentu tej książki i współpracuj z Cyfroteką: