Read-write lock

Przypomnijmy rozwiązanie z wykładu (slajd 131):

możnaCzytać, możnaPisać: ConditionVariable
czytelnicy, pisarze: int

def chcęCzytać():
if pisarze or !empty(możnaPisać):
wait(możnaCzytać)
czytelnicy++
signal(możnaCzytać) // Kaskadowe budzenie.

def koniecCzytania():
czytelnicy--
if czytelnicy == 0:
signal(możnaPisać)

def chcęPisać():
if czytelnicy || pisarze:
wait(możnaPisać)
pisanie++

def koniecPisania():
pisanie--;
if !empty(możnaCzytać):
signal(możnaCzytać)
else:
signal(możnaPisać)

Monitory w semantyce Mesa (w pthreads i przez to w praktycznie każdym popularniejszym języku programowania) różnią się semantyką od tych z wykładu i ćwiczeń:

Z powodu spontanicznego budzenia i wydajności, zamiast kaskadowego budzenia lepiej użyć broadcast:

możnaCzytać, możnaPisać: ConditionVariable
czytelnicy, pisarze: int

def chcęCzytać():
if pisarze or !empty(możnaPisać):
wait(możnaCzytać)
czytelnicy++

def koniecCzytania():
czytelnicy--
if czytelnicy == 0:
signal(możnaPisać)

def chcęPisać():
if czytelnicy || pisarze:
wait(możnaPisać)
pisanie++

def koniecPisania():
pisanie--;
if !empty(możnaCzytać):
broadcast(możnaCzytać)
else:
signal(możnaPisać)

Z powodu spontanicznego budzenia, musimy sprawdzać warunek w pętli. Nie wystarczy jednak wszystko zamienić na pętle. Niektóre warunki sprawdzaliśmy by zapewnić bezpieczeństwo, inne by ustalić priorytety i uniknąć zagłodzenia. Co złego się stanie jeśli wszystko zamienimy na pętle?:

def chcęCzytać():
while pisarze or !empty(możnaPisać):
wait(możnaCzytać)
czytelnicy++

def koniecCzytania():
czytelnicy--
if czytelnicy == 0:
signal(możnaPisać)

def chcęPisać():
while czytelnicy || pisarze:
wait(możnaPisać)
pisanie++

def koniecPisania():
pisanie--;
if !empty(możnaCzytać):
broadcast(możnaCzytać)
else:
signal(możnaPisać)

W sytuacji z wieloma pisarzami praktycznie zawsze ktoś czeka na pisanie, więc zagłodzilibyśmy czytelników. Jak to naprawić?

Rozwiązaniem jest sprawdzanie, czy czekają pisarze, tylko za pierwszym razem:

def chcęCzytać():
if pisarze or !empty(możnaPisać):
wait(możnaCzytać)
while pisarze:
wait(możnaCzytać)
czytelnicy++

Czyli nie ustępujemy pisarzom, którzy przyszli po nas.

Kolejna różnica w stosunku do semantyki z wykładu, to że bo nie mamy gwarancji dot. kolejności budzenia. W szczególności jeśli mamy wątek, który w kółko pisze, to w każdej iteracji kończąc pisanie budzi czytelników, ale sam kontynuuje działanie i ma dużą szansę szybciej w następnej iteracji wejść do monitora i znów zacząć pisać. Może więc zagłodzić pozostałe wątki. W praktyce zawsze w końcu odda miejsce czytelnikom, ale może się to dziać rzadziej niż byśmy chcieli.

Można użyć ogólnego rozwiązania, żeby wymusić słabą sprawiedliwość, czyli że po signal/broadcast priorytet mają mieć wątki, które w momencie signal/broadcast czekały na zmiennej warunkowej. Polega ono na dodaniu flagi np. signalling: boolean, która będzie mówiła, że zawołaliśmy signal/broadcast i do monitora powinien teraz wejść wątek, który czekał na zmiennej warunkowej. Flagę podnosimy przy każdym wywołaniu signal/broadcast, a opuszczamy wychodząc z wait(). Wątek który wchodzi do monitora i widzi podniesioną flagę, wie że teraz to nie on powinien był wejść do monitora, tylko ktoś czekający w wait(), więc sam ustępuje robiąc wait().

Czyli piszemy np.:

while !... or signalling:
wait()
signalling = False

if ...:
signalling = True
broadcast()

Ogólnie jednak nie ma jednego najlepszego rozwiązania problemu czytelników i pisarzy, wybór zależy od wymagań dot. sprawiedliwości i zagłodzeń, wydajności, proporcji czytelników do pisarzy, typowych odstępów między odczytami i między zapisami, itd. W dzisiejszym zadaniu, postaraj się, by liczba odczytów nie różniła się od liczby zapisów więcej niż 4-krotnie.