Reader-Writer Locks

Mit C++14 erhielt C++ Reader-Writer Locks. Die Idee ist einfach und überzeugend. Während beliebig viele Threads gleichzeitig lesend auf einen kritischen Bereich zugreifen dürfen, darf nur genau ein Thread auf diesen schreibend zugreifen.

Weniger Engpässe

Reader-Writer Locks lösen zwar nicht das prinzipielle Problem, dass mehrere Threads um den Zugriff auf einen kritischen Bereich konkurrieren. Sie helfen aber, die Auswirkungen des Engpasses zu minimieren. Am anschaulichsten zeigt dies ein Beispiel.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// readerWriterLock.cpp

#include <iostream>
#include <map>
#include <shared_mutex>
#include <string>
#include <thread>

std::map<std::string,int> teleBook{{"Dijkstra",1972},{"Scott",1976},{"Ritchie",1983}};

std::shared_timed_mutex teleBookMutex;

void addToTeleBook(const std::string& na, int tele){
  std::lock_guard<std::shared_timed_mutex> writerLock(teleBookMutex);
  std::cout << "\nSTARTING UPDATE " << na;
  std::this_thread::sleep_for(std::chrono::milliseconds(500));
  teleBook[na]= tele;
  std::cout << " ... ENDING UPDATE " << na << std::endl;
}

void printNumber(const std::string& na){
  std::shared_lock<std::shared_timed_mutex> readerLock(teleBookMutex);
  std::cout << na << ": " << teleBook[na];
}

int main(){

  std::cout << std::endl;

  std::thread reader1([]{ printNumber("Scott"); });
  std::thread reader2([]{ printNumber("Ritchie"); });
  std::thread w1([]{ addToTeleBook("Scott",1968); });
  std::thread reader3([]{ printNumber("Dijkstra"); });
  std::thread reader4([]{ printNumber("Scott"); });
  std::thread w2([]{ addToTeleBook("Bjarne",1965); });
  std::thread reader5([]{ printNumber("Scott"); });
  std::thread reader6([]{ printNumber("Ritchie"); });
  std::thread reader7([]{ printNumber("Scott"); });
  std::thread reader8([]{ printNumber("Bjarne"); });

  reader1.join();
  reader2.join();
  reader3.join();
  reader4.join();
  reader5.join();
  reader6.join();
  reader7.join();
  reader8.join();
  w1.join();
  w2.join();

  std::cout << std::endl;

  std::cout << "\nThe new telephone book" << std::endl;
  for (auto teleIt: teleBook){
    std::cout << teleIt.first << ": " << teleIt.second << std::endl;
  }

  std::cout << std::endl;

}

 

Die gemeinsame Variable, die es zu schützen gilt, ist das std::map teleBook in Zeile 9. In dem Telefonbuch teleBook wollen gleichzeitig acht Threads lesen und zwei Threads schreiben (Zeile 30 - 39). Damit die lesenden Threads gleichzeitig auf das Telefonbuch zugreifen können, verwenden sie einen std:.shared_lock<std::shared_timed_mutex> in Zeile 22. Im Gegensatz dazu benötigen die schreibenden Threads exklusiven Zugriff auf die gemeinsame Variable telebook. Die Exklusivität stellt der std::lock_guard<std::shared_time_mutex> in Zeile 14 sicher. Zum Abschuss gebe ich noch das aktualisierte Telefonbuch (Zeile 54 - 57) aus.

readerWriterLocks

Der Screenshot des Programms zeigt schön, dass die Ausgabe der lesenden im Gegensatz zu den schreibenden Threads vollkommen durcheinander ist. Das unterstreicht nur, dass die lesenden Threads gleichzeitig auf das gemeinsame Telefonbuch zugreifen.

Das war einfach. Zu einfach.

Undefiniertes Verhalten

Das Programm besitzt undefiniertes Verhalten. Welches? Ich bin auf die Kommentare gespannt. Sobald das Problem gelöst ist, werde ich die Lösung vorstellen. Nur ein kleiner Hinweis. Der verschränkte Zugriff der lesenden Threads auf std::cout ist wohldefiniert. 

Ein kritischer Wettlauf zeichnet sich dadurch aus, dass mindestens zwei Threads gleichzeitig auf ein gemeinsame Variable zugreifen wollen. Dabei versucht zu mindestens einer der Threads diese zu modifizieren. Diese Situation liegt in dem Programm vor. Eine Eigenheit der geordneten assoziativen Container in C++ ist es, dass das Lesen eines Elements den Container verändern kann. Dies passiert genau dann, wenn das Element in dem assoziativen Container nicht vorhanden ist. So wird in dem obigen Beispiel der Wert für "Bjarne" (Zeile 39) ausgelesen. Die std::map in Zeile 9 besitzt diesen Schlüssel aber nicht notwendigerweise. Ist der Schlüssel nicht vorhanden, wird das Paar ("Bjarne",0) in dem Container erzeugt. Genauer lässt sich dieses sehr unintuitive Verhalten auf cppreference.com nachlesen.

Wie geht's weiter?

Im nächsten Artikel geht es mit dem sicheren Initialisieren der Daten in Multithreading-Programmen weiter.

 

 

Do you want to get the source code? Source code repository

Kommentar schreiben


Rainer Grimm

Modernes C++

Neue Beiträge zum Wochenende

Beiträge-Archiv

Sourcecode

Besucher

Heute 94

Woche 607

Monat 4189

Insgesamt 51965

Aktuell sind 8 Gäste und keine Mitglieder online