Przykład dla boost shared_mutex (wiele odczytów / jeden zapis)?

116

Mam aplikację wielowątkową, która musi często czytać niektóre dane, a czasami te dane są aktualizowane. W tej chwili mutex zapewnia bezpieczny dostęp do tych danych, ale jest drogi, ponieważ chciałbym, aby wiele wątków mogło czytać jednocześnie i blokować je tylko wtedy, gdy potrzebna jest aktualizacja (wątek aktualizacji może czekać na zakończenie innych wątków) .

Myślę, że to właśnie boost::shared_mutexpowinno zrobić, ale nie jestem pewien, jak go używać, i nie znalazłem jasnego przykładu.

Czy ktoś ma prosty przykład, którego mógłbym użyć, aby rozpocząć?

kevin42
źródło
Przykład 1800 INFORMACJE jest poprawny. Zobacz także ten artykuł: Co nowego w Boost Threads .
Assaf Lavie
możliwy duplikat blokad czytnika / pisarza w C ++
cHao

Odpowiedzi:

102

Wygląda na to, że zrobiłbyś coś takiego:

boost::shared_mutex _access;
void reader()
{
  // get shared access
  boost::shared_lock<boost::shared_mutex> lock(_access);

  // now we have shared access
}

void writer()
{
  // get upgradable access
  boost::upgrade_lock<boost::shared_mutex> lock(_access);

  // get exclusive access
  boost::upgrade_to_unique_lock<boost::shared_mutex> uniqueLock(lock);
  // now we have exclusive access
}
1800 INFORMACJE
źródło
7
Po raz pierwszy używam boost i jestem nowicjuszem w C ++, więc może czegoś mi brakuje - ale we własnym kodzie musiałem określić typ, na przykład: boost :: shared_lock <shared_mutex> lock (_dostęp);
Ken Smith
2
Próbuję tego użyć, ale pojawia się błąd. brakujące argumenty szablonu przed „blokadą”. Jakieś pomysły?
Matt
2
@shaz Te są ograniczone, ale możesz je wydać wcześniej za pomocą .unlock (), jeśli potrzebujesz.
mmocny
4
Dodałem brakujące argumenty szablonu.
1
@raaj, możesz zdobyć upgrade_lock, ale aktualizacja do unikalnej blokady będzie blokowana do czasu zwolnienia shared_lock
1800 INFORMACJE
166

1800 INFORMACJE jest mniej więcej poprawne, ale jest kilka problemów, które chciałem poprawić.

boost::shared_mutex _access;
void reader()
{
  boost::shared_lock< boost::shared_mutex > lock(_access);
  // do work here, without anyone having exclusive access
}

void conditional_writer()
{
  boost::upgrade_lock< boost::shared_mutex > lock(_access);
  // do work here, without anyone having exclusive access

  if (something) {
    boost::upgrade_to_unique_lock< boost::shared_mutex > uniqueLock(lock);
    // do work here, but now you have exclusive access
  }

  // do more work here, without anyone having exclusive access
}

void unconditional_writer()
{
  boost::unique_lock< boost::shared_mutex > lock(_access);
  // do work here, with exclusive access
}

Zauważ również, że w przeciwieństwie do shared_lock, tylko jeden wątek może uzyskać upgrade_lock w jednym czasie, nawet jeśli nie jest zaktualizowany (co uważałem za niezręczne, kiedy na to wpadłem). Tak więc, jeśli wszyscy twoi czytelnicy są pisarzami warunkowymi, musisz znaleźć inne rozwiązanie.

mmocny
źródło
1
Wystarczy skomentować „inne rozwiązanie”. Kiedy wszyscy moi czytelnicy byli pisarzami warunkowymi, to, co zrobiłem, polegało na tym, że zawsze otrzymywali shared_lock, a kiedy potrzebowałem uaktualnić do uprawnień do zapisu, mógłbym .unlock () blokować czytnik i uzyskać nowy unique_lock. To skomplikuje logikę aplikacji, a teraz istnieje możliwość zmiany stanu przez innych pisarzy od momentu pierwszego przeczytania.
mmocny
8
Czy wiersz nie powinien boost::unique_lock< boost::shared_mutex > lock(lock);brzmieć boost::unique_lock< boost::shared_mutex > lock( _access ); ?
SteveWilkinson
4
To ostatnie zastrzeżenie jest bardzo dziwne. Jeśli tylko jeden wątek może pomieścić upgrade_lock naraz, jaka jest różnica między upgrade_lock a unique_lock?
Ken Smith
2
@Ken Nie było to zbyt jasne, ale zaletą upgrade_lock jest to, że nie blokuje, jeśli obecnie są nabyte niektóre shared_locks (przynajmniej nie dopóki nie zaktualizujesz do unique). Jednak drugi wątek do próby uzyskania upgrade_lock zostanie zablokowany, nawet jeśli pierwszy nie został zaktualizowany do unikalnego, czego się nie spodziewałem.
mmocny
6
Jest to znany problem ze wzmocnieniem. Wydaje się, że problem
Ofek Shilon
47

Od C ++ 17 (VS2015) możesz używać standardu blokad do odczytu i zapisu:

#include <shared_mutex>

typedef std::shared_mutex Lock;
typedef std::unique_lock< Lock > WriteLock;
typedef std::shared_lock< Lock > ReadLock;

Lock myLock;


void ReadFunction()
{
    ReadLock r_lock(myLock);
    //Do reader stuff
}

void WriteFunction()
{
     WriteLock w_lock(myLock);
     //Do writer stuff
}

W przypadku starszej wersji możesz użyć funkcji boost z tą samą składnią:

#include <boost/thread/locks.hpp>
#include <boost/thread/shared_mutex.hpp>

typedef boost::shared_mutex Lock;
typedef boost::unique_lock< Lock >  WriteLock;
typedef boost::shared_lock< Lock >  ReadLock;
Yochai Timmer
źródło
5
Powiedziałbym też typedef boost::unique_lock< Lock > WriteLock; typedef boost::shared_lock< Lock > ReadLock;.
winorośl
6
Nie trzeba dołączać całego pliku thread.hpp. Jeśli potrzebujesz tylko zamków, dołącz zamki. To nie jest implementacja wewnętrzna. Ogranicz zawartość do minimum.
Yochai Timmer
5
Zdecydowanie najprostsza implementacja, ale myślę, że mylące jest nazywanie zarówno muteksów, jak i blokad jako blokad. Muteks to mutex, blokada to coś, co utrzymuje go w stanie zablokowanym.
Tim MB,
17

Aby dodać więcej informacji empirycznych, badałem cały problem blokad z możliwością aktualizacji i przykład dla zwiększenia shared_mutex (wiele odczytów / jeden zapis)? jest dobrą odpowiedzią, dodając ważne informacje, że tylko jeden wątek może mieć upgrade_lock, nawet jeśli nie jest zaktualizowany, co jest ważne, ponieważ oznacza to, że nie można zaktualizować wspólnej blokady do unikalnej blokady bez uprzedniego zwolnienia wspólnej blokady. (Zostało to omówione w innym miejscu, ale najciekawszy wątek jest tutaj http://thread.gmane.org/gmane.comp.lib.boost.devel/214394 )

Jednak znalazłem ważną (nieudokumentowaną) różnicę między wątkiem oczekującym na aktualizację do blokady (tj. Musi czekać, aż wszyscy czytelnicy zwolnią współdzieloną blokadę) a blokadą pisarza czekającą na to samo (tj. Unique_lock).

  1. Wątek, który czeka na unique_lock na shared_mutex, blokuje przychodzących nowych czytelników, muszą oni czekać na żądanie pisarzy. Gwarantuje to, że czytelnicy nie głodują pisarzy (jednak uważam, że pisarze mogą głodować czytelników).

  2. Wątek, który czeka na aktualizację upgradeable_lock, pozwala innym wątkom uzyskać współdzieloną blokadę, więc ten wątek może zostać zagłodzony, jeśli czytelnicy będą bardzo często.

Jest to ważna kwestia do rozważenia i prawdopodobnie powinna zostać udokumentowana.

Jim Morris
źródło
3
W Terekhov algorithmgwarantuje, że w 1., pisarz nie może głodować czytelników. Zobacz to . Ale 2.to prawda. Upgrade_lock nie gwarantuje uczciwości. Zobacz to .
JonasVautherin
2

Użyj semafora z liczbą równą liczbie czytelników. Niech każdy czytelnik policzy semafor po jednym policzeniu, aby móc czytać, tak aby wszyscy mogli czytać w tym samym czasie. Następnie pozwól pisarzowi wziąć WSZYSTKIE liczby semaforów przed napisaniem. Powoduje to, że piszący czeka na zakończenie wszystkich odczytów, a następnie blokuje odczyty podczas pisania.

R Virzi
źródło
(1) Jak sprawić, by pisarz atomowo zmniejszył liczbę o dowolną wartość ? (2) Jeśli autor w jakiś sposób zmniejszy liczbę do zera, w jaki sposób czeka, aż uruchomione już czytniki zakończą pisanie?
Ofek Shilon,
Zły pomysł: jeśli dwóch autorów próbuje uzyskać dostęp jednocześnie, możesz mieć impas.
Caduchon
2

Świetna odpowiedź Jima Morrisa. Natknąłem się na to i zajęło mi trochę czasu, zanim zrozumiałem. Oto prosty kod, który pokazuje, że po przesłaniu „żądania” wzmocnienia unique_lock (wersja 1.54) wszystkie żądania shared_lock zostaną zablokowane. Jest to bardzo interesujące, ponieważ wydaje mi się, że wybór między unique_lock i upgradeable_lock pozwala na to, czy chcemy pisać priorytet, czy nie.

Również (1) w poście Jima Morrisa wydaje się temu zaprzeczać: Boost shared_lock. Czytaj preferowane?

#include <iostream>
#include <boost/thread.hpp>

using namespace std;

typedef boost::shared_mutex Lock;
typedef boost::unique_lock< Lock > UniqueLock;
typedef boost::shared_lock< Lock > SharedLock;

Lock tempLock;

void main2() {
    cout << "10" << endl;
    UniqueLock lock2(tempLock); // (2) queue for a unique lock
    cout << "11" << endl;
    boost::this_thread::sleep(boost::posix_time::seconds(1));
    lock2.unlock();
}

void main() {
    cout << "1" << endl;
    SharedLock lock1(tempLock); // (1) aquire a shared lock
    cout << "2" << endl;
    boost::thread tempThread(main2);
    cout << "3" << endl;
    boost::this_thread::sleep(boost::posix_time::seconds(3));
    cout << "4" << endl;
    SharedLock lock3(tempLock); // (3) try getting antoher shared lock, deadlock here
    cout << "5" << endl;
    lock1.unlock();
    lock3.unlock();
}
dale1209
źródło
Właściwie mam problem ze zrozumieniem, dlaczego powyższy kod blokuje się, podczas gdy kod w [ stackoverflow.com/questions/12082405/… działa.
dale1209
1
W rzeczywistości zakleszcza się w (2), a nie w (3), ponieważ (2) czeka, aż (1) zwolni blokadę. Pamiętaj: aby uzyskać unikalną blokadę, musisz poczekać, aż wszystkie istniejące wspólne blokady się zakończą.
JonasVautherin
@JonesV, nawet jeśli (2) czeka na zakończenie wszystkich współdzielonych blokad, nie byłby to zakleszczenie, ponieważ jest to inny wątek niż ten, który uzyskał (1), gdyby linia (3) nie istniała, program zakończyć bez zakleszczeń.
SagiLow,