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)