Cyfroteka.pl

klikaj i czytaj online

Cyfro
Czytomierz
00377 007837 15362660 na godz. na dobę w sumie
Język C++ i przetwarzanie współbieżne w akcji - książka
Język C++ i przetwarzanie współbieżne w akcji - książka
Autor: Liczba stron: 576
Wydawca: Helion Język publikacji: polski
ISBN: 978-83-246-5086-6 Data wydania:
Lektor:
Kategoria: ebooki >> komputery i informatyka >> programowanie >> c++ - programowanie
Porównaj ceny (książka, ebook, audiobook).

Odkryj wszystkie tajemnice wielowątkowych aplikacji!

Współbieżne przetwarzanie danych to największe wyzwanie dla programisty. Na każdym kroku czyhają na niego najbardziej wymyślne pułapki, a wykrycie pomyłki stanowi nie lada wyzwanie. Każdy programista wzdryga się na samą myśl o implementacji wielowątkowych rozwiązań. Nie musi tak być!

Dzięki tej książce poradzisz sobie z większością zadań i zwinnie ominiesz zastawione pułapki. W trakcie lektury dowiesz się, jak zidentyfikować zadania, w których zastosowanie współbieżności ma sens, oraz jak zarządzać wątkami. Ponadto nauczysz się chronić współdzielone dane oraz synchronizować współbieżne operacje. Duży nacisk został tu położony na zagadnienia związane z projektowaniem współbieżnych struktur danych oraz kodu. Osobny rozdział poświęcono debugowaniu aplikacji wielowątkowych. Książka ta jest długo oczekiwaną pozycją, która ułatwi codzienne życie programistom C++.

Dzięki tej książce:

Sprawdź, jak tworzyć niezawodne oprogramowanie wielowątkowe!

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

Darmowy fragment publikacji:

Tytuł oryginału: C++ Concurrency in Action: Practical Multithreading Tłumaczenie: Mikołaj Szczepaniak Projekt okładki: Anna Mitka Materiały graficzne na okładce zostały wykorzystane za zgodą Shutterstock Images LLC. ISBN: 978-83-246-5086-6 Original edition copyright © 2012 by Manning Publications Co. All rights reserved. Polish edition copyright © 2013 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 Wydawnictwo HELION 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 Wydawnictwo HELION nie ponoszą również żadnej odpowiedzialności za ewentualne szkody wynikłe z wykorzystania informacji zawartych w książce. Wydawnictwo HELION 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?gimpbi 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 19 1.1. Czym jest wspóïbieĝnoĂÊ? ........................................................................................................... 20 1.1.1. WspóïbieĝnoĂÊ w systemach komputerowych ................................................................. 20 1.1.2. Modele wspóïbieĝnoĂci ..................................................................................................... 22 1.2. Dlaczego warto stosowaÊ wspóïbieĝnoĂÊ? ................................................................................. 25 1.2.1. Stosowanie wspóïbieĝnoĂci do podziaïu zagadnieñ ......................................................... 25 1.2.2. Stosowanie wspóïbieĝnoĂci do podniesienia wydajnoĂci ................................................. 26 1.2.3. Kiedy nie naleĝy stosowaÊ wspóïbieĝnoĂci ....................................................................... 27 1.3. WspóïbieĝnoĂÊ i wielowÈtkowoĂÊ w jÚzyku C++ .................................................................... 28 1.3.1. Historia przetwarzania wielowÈtkowego w jÚzyku C++ ................................................ 29 1.3.2. Obsïuga wspóïbieĝnoĂci w nowym standardzie ............................................................... 30 1.3.3. EfektywnoĂÊ biblioteki wÈtków jÚzyka C++ .................................................................. 30 1.3.4. Mechanizmy zwiÈzane z poszczególnymi platformami ................................................... 32 1.4. Do dzieïa! ...................................................................................................................................... 32 1.4.1. „Witaj Ăwiecie wspóïbieĝnoĂci” ......................................................................................... 32 1.5. Podsumowanie .............................................................................................................................. 34 35 2.1. Podstawowe zarzÈdzanie wÈtkami ............................................................................................. 36 2.1.1 Uruchamianie wÈtku .......................................................................................................... 36 2.1.2. Oczekiwanie na zakoñczenie wÈtku .................................................................................. 39 2.1.3. Oczekiwanie w razie wystÈpienia wyjÈtku ....................................................................... 39 2.1.4. Uruchamianie wÈtków w tle .............................................................................................. 42 2.2. Przekazywanie argumentów do funkcji wÈtku ......................................................................... 43 2.3. Przenoszenie wïasnoĂci wÈtku .................................................................................................... 46 2.4. Wybór liczby wÈtków w czasie wykonywania ........................................................................... 49 2.5. Identyfikowanie wÈtków ............................................................................................................. 52 2.6. Podsumowanie .............................................................................................................................. 54 55 3.1. Problemy zwiÈzane ze wspóïdzieleniem danych przez wÈtki ................................................. 56 3.1.1. Sytuacja wyĂcigu ................................................................................................................ 58 3.1.2. Unikanie problematycznych sytuacji wyĂcigu .................................................................. 59 3.2. Ochrona wspóïdzielonych danych za pomocÈ muteksów ........................................................ 60 3.2.1. Stosowanie muteksów w jÚzyku C++ ............................................................................. 60 3.2.2. Projektowanie struktury kodu z myĂlÈ o ochronie wspóïdzielonych danych ................. 62 Rozdziaï 2. ZarzÈdzanie wÈtkami Rozdziaï 3. Wspóïdzielenie danych przez wÈtki 6 Spis treĂci Rozdziaï 4. Synchronizacja wspóïbieĝnych operacji 3.2.3. Wykrywanie sytuacji wyĂcigu zwiÈzanych z interfejsami ................................................ 63 3.2.4. Zakleszczenie: problem i rozwiÈzanie .............................................................................. 71 3.2.5. Dodatkowe wskazówki dotyczÈce unikania zakleszczeñ ................................................. 73 3.2.6. Elastyczne blokowanie muteksów za pomocÈ szablonu std::unique_lock ...................... 79 3.2.7. Przenoszenie wïasnoĂci muteksu pomiÚdzy zasiÚgami .................................................... 80 3.2.8. Dobór wïaĂciwej szczegóïowoĂci blokad .......................................................................... 82 3.3. Alternatywne mechanizmy ochrony wspóïdzielonych danych ................................................ 84 3.3.1. Ochrona wspóïdzielonych danych podczas inicjalizacji .................................................. 84 3.3.2. Ochrona rzadko aktualizowanych struktur danych .......................................................... 88 3.3.3. Blokowanie rekurencyjne .................................................................................................. 90 3.4. Podsumowanie .............................................................................................................................. 91 93 4.1. Oczekiwanie na zdarzenie lub inny warunek ........................................................................... 94 4.1.1. Oczekiwanie na speïnienie warunku za pomocÈ zmiennych warunkowych .................. 95 4.1.2. Budowa kolejki gwarantujÈcej bezpieczne przetwarzanie wielowÈtkowe przy uĝyciu zmiennych warunkowych .............................................................................. 97 4.2. Oczekiwanie na jednorazowe zdarzenia za pomocÈ przyszïoĂci ........................................... 102 4.2.1. Zwracanie wartoĂci przez zadania wykonywane w tle ................................................... 103 4.2.2. WiÈzanie zadania z przyszïoĂciÈ ...................................................................................... 106 4.2.3. Obietnice (szablon std::promise) ..................................................................................... 109 4.2.4. Zapisywanie wyjÈtku na potrzeby przyszïoĂci ................................................................ 111 4.2.5. Oczekiwanie na wiele wÈtków ........................................................................................ 112 4.3. Oczekiwanie z limitem czasowym ............................................................................................ 115 4.3.1. Zegary ............................................................................................................................... 115 4.3.2. Okresy ............................................................................................................................... 117 4.3.3. Punkty w czasie ................................................................................................................ 118 4.3.4. Funkcje otrzymujÈce limity czasowe .............................................................................. 120 4.4. Upraszczanie kodu za pomocÈ technik synchronizowania operacji ..................................... 121 4.4.1. Programowanie funkcyjne przy uĝyciu przyszïoĂci ....................................................... 122 4.4.2. Synchronizacja operacji za pomocÈ przesyïania komunikatów ..................................... 127 4.5. Podsumowanie ............................................................................................................................ 131 133 5.1. Podstawowe elementy modelu pamiÚci ................................................................................... 134 5.1.1. Obiekty i miejsca w pamiÚci ............................................................................................ 134 5.1.2. Obiekty, miejsca w pamiÚci i przetwarzanie wspóïbieĝne ............................................ 135 5.1.3. KolejnoĂÊ modyfikacji ...................................................................................................... 136 5.2. Operacje i typy atomowe jÚzyka C++ .................................................................................... 137 5.2.1. Standardowe typy atomowe ............................................................................................ 138 5.2.2. Operacje na typie std::atomic_flag .................................................................................. 141 5.2.3. Operacje na typie std::atomic bool ............................................................................ 143 5.2.4. Operacje na typie std::atomic T* — arytmetyka wskaěników ................................. 146 5.2.5. Operacje na standardowych atomowych typach caïkowitoliczbowych ........................ 147 5.2.6. Gïówny szablon klasy std::atomic ............................................................................. 147 5.2.7. Wolne funkcje dla operacji atomowych .......................................................................... 150 5.3. Synchronizacja operacji i wymuszanie ich porzÈdku ............................................................ 151 5.3.1. Relacja synchronizacji ...................................................................................................... 152 5.3.2. Relacja poprzedzania ....................................................................................................... 154 5.3.3. PorzÈdkowanie pamiÚci na potrzeby operacji atomowych ............................................ 155 5.3.4. Sekwencje zwalniania i relacja synchronizacji ............................................................... 175 Rozdziaï 5. Model pamiÚci jÚzyka C++ i operacje na typach atomowych Spis treĂci 7 Rozdziaï 6. Projektowanie wspóïbieĝnych struktur danych przy uĝyciu blokad 5.3.5. Ogrodzenia ....................................................................................................................... 178 5.3.6. PorzÈdkowanie operacji nieatomowych za pomocÈ operacji atomowych ..................... 180 5.4. Podsumowanie ............................................................................................................................ 182 183 6.1. Co oznacza projektowanie struktur danych pod kÈtem wspóïbieĝnoĂci? ............................ 184 6.1.1. Wskazówki dotyczÈce projektowania wspóïbieĝnych struktur danych ........................ 185 6.2. Projektowanie wspóïbieĝnych struktur danych przy uĝyciu blokad .................................... 186 6.2.1. Stos gwarantujÈcy bezpieczeñstwo przetwarzania wielowÈtkowego przy uĝyciu blokad ........................................................................................................... 187 6.2.2. Kolejka gwarantujÈca bezpieczeñstwo przetwarzania wielowÈtkowego przy uĝyciu blokad i zmiennych warunkowych ............................................................. 190 6.2.3. Kolejka gwarantujÈca bezpieczeñstwo przetwarzania wielowÈtkowego przy uĝyciu szczegóïowych blokad i zmiennych warunkowych .................................... 194 6.3. Projektowanie zïoĝonych struktur danych przy uĝyciu blokad ............................................. 207 6.3.1. Implementacja tablicy wyszukiwania gwarantujÈcej bezpieczeñstwo przetwarzania wielowÈtkowego przy uĝyciu blokad ...................................................... 207 6.3.2. Implementacja listy gwarantujÈcej bezpieczeñstwo przetwarzania Rozdziaï 7. Projektowanie wspóïbieĝnych struktur danych bez blokad wielowÈtkowego przy uĝyciu blokad .............................................................................. 213 6.4. Podsumowanie ............................................................................................................................ 218 219 7.1. Definicje i ich praktyczne znaczenie ....................................................................................... 220 7.1.1. Rodzaje nieblokujÈcych struktur danych ........................................................................ 220 7.1.2. Struktury danych bez blokad ........................................................................................... 221 7.1.3. Struktury danych bez oczekiwania ................................................................................. 222 7.1.4. Zalety i wady struktur danych bez blokad ...................................................................... 222 7.2. Przykïady struktur danych bez blokad .................................................................................... 223 7.2.1. Implementacja stosu gwarantujÈcego bezpieczeñstwo przetwarzania wielowÈtkowego bez blokad ............................................................................................ 224 7.2.2. Eliminowanie niebezpiecznych wycieków — zarzÈdzanie pamiÚciÈ w strukturach danych bez blokad .................................................................................... 228 7.2.3. Wykrywanie wÚzïów, których nie moĝna odzyskaÊ, za pomocÈ wskaěników ryzyka ........ 233 7.2.4. Wykrywanie uĝywanych wÚzïów metodÈ zliczania referencji .......................................... 242 7.2.5. Zmiana modelu pamiÚci uĝywanego przez operacje na stosie bez blokad ................... 247 7.2.6. Implementacja kolejki gwarantujÈcej bezpieczeñstwo przetwarzania wielowÈtkowego bez blokad ............................................................................................ 252 7.3. Wskazówki dotyczÈce pisania struktur danych bez blokad ................................................... 264 7.3.1. Wskazówka: na etapie tworzenia prototypu naleĝy stosowaÊ tryb std::memory_order_seq_cst ............................................................................................ 265 7.3.2. Wskazówka: naleĝy uĝywaÊ schematu odzyskiwania pamiÚci bez blokad .................... 265 7.3.3 Wskazówka: naleĝy unikaÊ problemu ABA .................................................................... 266 7.3.4. Wskazówka: naleĝy identyfikowaÊ pÚtle aktywnego oczekiwania i wykorzystywaÊ Rozdziaï 8. Projektowanie wspóïbieĝnego kodu czas bezczynnoĂci na wspieranie innego wÈtku ............................................................. 267 7.4. Podsumowanie ............................................................................................................................ 267 269 8.1. Techniki dzielenia pracy pomiÚdzy wÈtki ............................................................................... 270 8.1.1. Dzielenie danych pomiÚdzy wÈtki przed rozpoczÚciem przetwarzania ....................... 271 8.1.2. Rekurencyjne dzielenie danych ...................................................................................... 272 8.1.3. Dzielenie pracy wedïug typu zadania ............................................................................. 276 8 Spis treĂci Rozdziaï 9. Zaawansowane zarzÈdzanie wÈtkami 8.2. Czynniki wpïywajÈce na wydajnoĂÊ wspóïbieĝnego kodu ..................................................... 279 8.2.1. Liczba procesorów ........................................................................................................... 280 8.2.2. Wspóïzawodnictwo o dane i ping-pong bufora .............................................................. 281 8.2.3. Faïszywe wspóïdzielenie ................................................................................................. 284 8.2.4. Jak blisko naleĝy rozmieĂciÊ dane? ................................................................................. 285 8.2.5. Nadsubskrypcja i zbyt intensywne przeïÈczanie zadañ ................................................. 285 8.3. Projektowanie struktur danych pod kÈtem wydajnoĂci przetwarzania wielowÈtkowego ..... 286 8.3.1. Podziaï elementów tablicy na potrzeby zïoĝonych operacji .......................................... 287 8.3.2. Wzorce dostÚpu do danych w pozostaïych strukturach ................................................. 289 8.4. Dodatkowe aspekty projektowania wspóïbieĝnych struktur danych ................................... 291 8.4.1. Bezpieczeñstwo wyjÈtków w algorytmach równolegïych .............................................. 291 8.4.2. SkalowalnoĂÊ i prawo Amdahla ....................................................................................... 298 8.4.3. Ukrywanie opóěnieñ za pomocÈ wielu wÈtków .............................................................. 300 8.4.4. Skracanie czasu reakcji za pomocÈ technik przetwarzania równolegïego .................... 301 8.5. Projektowanie wspóïbieĝnego kodu w praktyce ..................................................................... 303 8.5.1. Równolegïa implementacja funkcji std::for_each ........................................................... 304 8.5.2. Równolegïa implementacja funkcji std::find .................................................................. 306 8.5.3. Równolegïa implementacja funkcji std::partial_sum ..................................................... 312 8.6. Podsumowanie ............................................................................................................................ 322 323 9.1. Pule wÈtków ................................................................................................................................ 324 9.1.1. Najprostsza moĝliwa pula wÈtków .................................................................................. 324 9.1.2. Oczekiwanie na zadania wysyïane do puli wÈtków ........................................................ 327 9.1.3. Zadania oczekujÈce na inne zadania ............................................................................... 330 9.1.4. Unikanie wspóïzawodnictwa w dostÚpie do kolejki zadañ ............................................ 333 9.1.5. Wykradanie zadañ ............................................................................................................ 335 9.2. Przerywanie wykonywania wÈtków .......................................................................................... 340 9.2.1. Uruchamianie i przerywanie innego wÈtku .................................................................... 340 9.2.2. Wykrywanie przerwania wÈtku ....................................................................................... 342 9.2.3. Przerywanie oczekiwania na zmiennÈ warunkowÈ ........................................................ 343 9.2.4. Przerywanie oczekiwania na zmiennÈ typu std::condition_variable_any ..................... 346 9.2.5. Przerywanie pozostaïych wywoïañ blokujÈcych ............................................................ 348 9.2.6. Obsïuga przerwañ ............................................................................................................ 349 9.2.7. Przerywanie zadañ wykonywanych w tle podczas zamykania aplikacji ........................ 350 9.3. Podsumowanie ............................................................................................................................ 352 353 10.1. Rodzaje bïÚdów zwiÈzanych z przetwarzaniem wspóïbieĝnym ............................................ 354 10.1.1. Niechciane blokowanie ............................................................................................... 354 10.1.2. Sytuacje wyĂcigu ......................................................................................................... 355 10.2. Techniki lokalizacji bïÚdów zwiÈzanych z przetwarzaniem wspóïbieĝnym ........................ 357 10.2.1. PrzeglÈdanie kodu w celu znalezienia ewentualnych bïÚdów .................................. 357 10.2.2. Znajdowanie bïÚdów zwiÈzanych z przetwarzaniem wspóïbieĝnym poprzez testowanie kodu ................................................................................................. 359 10.2.3. Projektowanie kodu pod kÈtem ïatwoĂci testowania ................................................. 361 10.2.4. Techniki testowania wielowÈtkowego kodu .............................................................. 363 10.2.5. Projektowanie struktury wielowÈtkowego kodu testowego ...................................... 366 10.2.6. Testowanie wydajnoĂci wielowÈtkowego kodu ......................................................... 369 10.3. Podsumowanie ............................................................................................................................ 370 Rozdziaï 10. Testowanie i debugowanie aplikacji wielowÈtkowych Spis treĂci 9 Dodatek A Krótki przeglÈd wybranych elementów jÚzyka C++11 371 A.1. Referencje do r-wartoĂci ........................................................................................................... 371 A.1.1. Semantyka przenoszenia danych ..................................................................................... 372 A.1.2. Referencje do r-wartoĂci i szablony funkcji .................................................................... 375 A.2. UsuniÚte funkcje ......................................................................................................................... 376 A.3. Funkcje domyĂlne ...................................................................................................................... 377 A.4. Funkcje constexpr ...................................................................................................................... 381 A.4.1. Wyraĝenia constexpr i typy definiowane przez uĝytkownika ........................................ 382 A.4.2. Obiekty constexpr ............................................................................................................ 385 A.4.3. Wymagania dotyczÈce funkcji constexpr ........................................................................ 385 A.4.4. Sïowo constexpr i szablony .............................................................................................. 386 A.5. Funkcje lambda .......................................................................................................................... 386 A.5.1. Funkcje lambda odwoïujÈce siÚ do zmiennych lokalnych ............................................. 388 A.6. Szablony zmiennoargumentowe ............................................................................................... 391 A.6.1. Rozwijanie paczki parametrów ....................................................................................... 392 A.7. Automatyczne okreĂlanie typu zmiennej ................................................................................. 395 A.8. Zmienne lokalne wÈtków ........................................................................................................... 396 A.9. Podsumowanie ............................................................................................................................ 397 399 Dodatek B Krótkie zestawienie bibliotek przetwarzania wspóïbieĝnego Dodatek C Framework przekazywania komunikatów i kompletny przykïad Dodatek D Biblioteka wÈtków jÚzyka C++ implementacji systemu bankomatu 401 419 D.1. Nagïówek chrono ................................................................................................................. 419 D.1.1. Szablon klasy std::chrono::duration ............................................................................ 420 D.1.2. Szablon klasy std::chrono::time_point ....................................................................... 429 D.1.3. Klasa std::chrono::system_clock ................................................................................. 431 D.1.4. Klasa std::chrono::steady_clock .................................................................................. 433 D.1.5. Definicja typu std::chrono::high_resolution_clock ................................................... 435 D.2. Nagïówek condition_variable ............................................................................................ 435 D.2.1. Klasa std::condition_variable ...................................................................................... 436 D.2.2. Klasa std::condition_variable_any .............................................................................. 444 D.3. Nagïówek atomic ................................................................................................................. 452 D.3.1. Definicje typów std::atomic_xxx ................................................................................ 454 D.3.2. Makra ATOMIC_xxx_LOCK_FREE ........................................................................ 454 D.3.3. Makro ATOMIC_VAR_INIT ..................................................................................... 455 D.3.4. Typ wyliczeniowy std::memory_order ....................................................................... 455 D.3.5. Funkcja std::atomic_thread_fence ............................................................................. 456 D.3.6. Funkcja std::atomic_signal_fence .............................................................................. 457 D.3.7. Klasa std::atomic_flag .................................................................................................. 457 Szablon klasy std::atomic ............................................................................................ 460 D.3.8. D.3.9. Specjalizacje szablonu std::atomic ............................................................................. 471 D.3.10. Specjalizacje szablonu std::atomic typ-caïkowitoliczbowy ................................. 472 D.4. Nagïówek future .................................................................................................................. 489 Szablon klasy std::future ............................................................................................. 490 Szablon klasy std::shared_future ................................................................................ 495 Szablon klasy std::packaged_task ............................................................................... 501 Szablon klasy std::promise .......................................................................................... 507 Szablon funkcji std::async ........................................................................................... 513 D.4.1. D.4.2. D.4.3. D.4.4. D.4.5. 10 Spis treĂci D.5. Nagïówek mutex .................................................................................................................. 514 D.5.1. Klasa std::mutex .......................................................................................................... 515 D.5.2. Klasa std::recursive_mutex ......................................................................................... 518 D.5.3. Klasa std::timed_mutex ............................................................................................... 520 D.5.4. Klasa std::recursive_timed_mutex ............................................................................. 524 Szablon klasy std::lock_guard ..................................................................................... 529 D.5.5. Szablon klasy std::unique_lock ................................................................................... 530 D.5.6. Szablon funkcji std::lock ............................................................................................. 540 D.5.7. D.5.8. Szablon funkcji std::try_lock ....................................................................................... 541 D.5.9. Klasa std::once_flag ..................................................................................................... 541 D.5.10. Szablon funkcji std::call_once .................................................................................... 542 D.6. Nagïówek ratio ..................................................................................................................... 543 D.6.1. Szablon klasy std::ratio ................................................................................................ 544 D.6.2. Alias szablonu std::ratio_add ...................................................................................... 544 D.6.3. Alias szablonu std::ratio_subtract ............................................................................... 545 D.6.4. Alias szablonu std::ratio_multiply .............................................................................. 545 D.6.5. Alias szablonu std::ratio_divide .................................................................................. 546 Szablon klasy std::ratio_equal ..................................................................................... 547 D.6.6. D.6.7. Szablon klasy std::ratio_not_equal ............................................................................. 547 Szablon klasy std::ratio_less ........................................................................................ 547 D.6.8. D.6.9. Szablon klasy std::ratio_greater .................................................................................. 548 D.6.10. Szablon klasy std::ratio_less_equal ............................................................................ 548 D.6.11. Szablon klasy std::ratio_greater_equal ....................................................................... 548 D.7. Nagïówek thread ................................................................................................................. 549 D.7.1. Klasa std::thread .......................................................................................................... 549 D.7.2. Przestrzeñ nazw std::this_thread ................................................................................ 558 561 563 Materiaïy dodatkowe Skorowidz Synchronizacja wspóïbieĝnych operacji W tym rozdziale zostaną omówione nastĊpujące zagadnienia: Q oczekiwanie na zdarzenie; Q oczekiwanie na jednorazowe zdarzenia za pomocą przyszáoĞci; Q oczekiwanie z limitem czasowym; Q 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. W tym rozdziale omówiÚ techniki oczekiwania na zdarzenia przy uĝyciu zmiennych warunkowych oraz sposoby upraszczania synchronizacji operacji za pomocÈ przyszïoĂci. 94 4.1. ROZDZIAà 4. Synchronizacja wspóïbieĝnych operacji 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(); std::this_thread::sleep_for(std::chrono::milliseconds(100)); lk.lock(); } } Wywoïanie funkcji w pÚtli odblokowuje muteks i ponownie blokuje ten muteks po wyjĂciu z tego stanu ma szanse uzyskania dostÚpu do flagi i jej ustawienia. Odblokowuje muteks Ponownie blokuje muteks Czeka 100 ms przed przejĂciem w stan uĂpienia — dziÚki temu drugi wÈtek 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 4.1. Oczekiwanie na zdarzenie lub inny warunek 95 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; std::condition_variable data_cond; void data_preparation_thread() { while(more_data_to_prepare()) { data_chunk const data=prepare_data(); 96 ROZDZIAà 4. Synchronizacja wspóïbieĝnych operacji std::lock_guard std::mutex lk(mut); data_queue.push(data); data_cond.notify_one(); } } void data_processing_thread() { while(true) { std::unique_lock std::mutex lk(mut); data_cond.wait( lk,[]{return !data_queue.empty();}); data_chunk data=data_queue.front(); data_queue.pop(); lk.unlock(); process(data); if(is_last_chunk(data)) break; } } Na poczÈtku kodu zdefiniowano kolejkÚ , 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 . 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 . 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 — 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È- . Funkcje lambda to stosunkowo nowy element pieniem do dalszego przetwarzania (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. 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 4.1. Oczekiwanie na zdarzenie lub inny warunek 97 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 , 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. 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 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. 98 ROZDZIAà 4. Synchronizacja wspóïbieĝnych operacji 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 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 4.1. Oczekiwanie na zdarzenie lub inny warunek 99 { 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); std::shared_ptr T try_pop(); 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() zapisuje pobranÈ wartoĂÊ we wskazywanej zmiennej, tak aby moĝna byïo uĝyÊ tej wartoĂci w roli statusu; funkcja zwraca wartoĂÊ true, jeĂli uzyskaïa jakÈĂ wartoĂÊ — w przeciwnym razie funkcja zwraca wartoĂÊ false (patrz nie moĝe dziaïaÊ w ten sam spo- czÚĂÊ A.2 dodatku A). Druga przeciÈĝona wersja sób, poniewaĝ natychmiast zwraca uzyskanÈ wartoĂÊ. JeĂli jednak funkcja nie uzyskaï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). 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) { 100 ROZDZIAà 4. Synchronizacja wspóïbieĝnych operacji 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; void data_preparation_thread() { while(more_data_to_prepare()) { data_chunk const data=prepare_data(); data_queue.push(data); } } void data_processing_thread() { while(true) { data_chunk data; data_queue.wait_and_pop(data); process(data); if(is_last_chunk(data)) break; } } Muteks i zmienna warunkowa sÈ teraz elementami skïadowymi obiektu klasy threadsafe_ , a wywoïanie ´queue, zatem nie jest potrzebne stosowanie odrÚbnych zmiennych funkcji push() nie wymaga zewnÚtrznych mechanizmów synchronizacji . Jak widaÊ, takĝe funkcja wait_and_pop() uwzglÚdnia stan zmiennej warunkowej . 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; std::queue T data_queue; std::condition_variable data_cond; public: threadsafe_queue() {} Muteks musi byü modyfikowalny 4.1. Oczekiwanie na zdarzenie lub inny warunek 101 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; 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(); } }; 102 ROZDZIAà 4. Synchronizacja wspóïbieĝnych operacji 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) , 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 instancji wÈtku prze- twarzajÈcego dane. Po przygotowaniu nowych danych wywoïanie funkcji notify_one() spowoduje, ĝe jeden z wÈtków aktualnie wykonujÈcych funkcjÚ wait() sprawdzi waru- nek. Poniewaĝ do struktury data_queue wïaĂnie dodano nowe dane, funkcja wait() zwróci sterowanie. 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 wszystkie 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È. 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. 4.2. Oczekiwanie na jednorazowe zdarzenia za pomocÈ przyszïoĂci 103 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 jedynÈ instancjÈ odwoïujÈcÈ siÚ do powiÈzanego zdarzenia, natomiast do jednego zdarzenia moĝe siÚ odwoïywaÊ wiele instancji typu std::shared_future. W drugim przypadku wszystkie instancje 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 zaprojektowano 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È mecha- nizmów synchronizowanego dostÚpu. JeĂli wiele wÈtków potrzebuje dostÚpu do jednego obiektu przyszïoĂci, naleĝy chroniÊ ten dostÚp za pomocÈ muteksu lub innego mecha- nizmu 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. 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 104 ROZDZIAà 4. Synchronizacja wspóïbieĝnych operacji 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 sporym uïatwieniem jest uĝycie szablonu funkcji std::async (zadeklarowanego w pliku nagïó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. 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. 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. 4.2. Oczekiwanie na jed
Pobierz darmowy fragment (pdf)

Gdzie kupić całą publikację:

Język C++ i przetwarzanie współbieżne w akcji
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ą: