2016-12-16 20 views
7

boost::shared_mutex lub std::shared_mutex (C++ 17) może być używana dla pojedynczego programu piszącego, dostęp do wielu czytników. Jako ćwiczenie edukacyjne, stworzyłem prostą implementację, która wykorzystuje spinlocking i ma inne ograniczenia (np. Polityka uczciwości), ale oczywiście nie jest przeznaczona do użycia w rzeczywistych aplikacjach.Implementacja C++ shared_mutex

Pomysł polega na tym, że muteks zachowuje liczbę referencyjną równą zero, jeśli żaden wątek nie trzyma blokady. Jeśli> 0, wartość reprezentuje liczbę czytników, które mają dostęp. Jeśli -1, pojedynczy program piszący ma dostęp.

Czy jest to poprawna implementacja (w szczególności przy użyciu minimalnej ilości pamięci), która nie zawiera wyścigów danych?

#include <atomic> 

class my_shared_mutex { 
    std::atomic<int> refcount{0}; 
public: 

    void lock() // write lock 
    { 
     int val; 
     do { 
      val = 0; // Can only take a write lock when refcount == 0 

     } while (!refcount.compare_exchange_weak(val, -1, std::memory_order_acquire)); 
     // can memory_order_relaxed be used if only a single thread takes write locks ? 
    } 

    void unlock() // write unlock 
    { 
     refcount.store(0, std::memory_order_release); 
    } 

    void lock_shared() // read lock 
    { 
     int val; 
     do { 
      do { 
       val = refcount.load(std::memory_order_relaxed); 

      } while (val == -1); // spinning until the write lock is released 

     } while (!refcount.compare_exchange_weak(val, val+1, std::memory_order_acquire)); 
    } 

    void unlock_shared() // read unlock 
    { 
     refcount.fetch_sub(1, std::memory_order_relaxed); 
    } 
}; 

Odpowiedz

3

(używam cmpxchg jako skrót do funkcji C++ compare_exchange_weak, nie cmpxchg instruction).

lock_shared zdecydowanie wygląda dobrze: kręci się przy odczycie i tylko próbując cmpxchg, gdy wartość jest znacznie lepsza dla wydajności niż wirowanie na cmpxchg. Chociaż myślę, że zostałeś do tego zmuszony z powodu poprawności, aby uniknąć zmiany od -1 do 0 i odblokowania blokady zapisu.

myślę unlock_shared należy używać mo_release, nie mo_relaxed, ponieważ musi zamówić ładunki z udostępnionego struktury danych, aby upewnić się pisarzem nie zacząć pisać, zanim wydarzy się ładunki z sekcji krytycznej czytnika. (LoadStore reordering jest rzeczą na słabo uporządkowanych architekturach, nawet jeśli x86 zmienia kolejność StoreLoad.) A Release operation will order preceding loads and keep them inside the critical section.


(w zapisie lock): // może memory_order_relaxed być stosowane, jeżeli tylko jeden wątek wykonuje napisać blokad?

Nie, trzeba jeszcze zachować zapisy wewnątrz sekcji krytycznej, więc cmpxchg nadal wymaga synchronizacji z (w terminologii C++) release-sklepów z unlock_shared.

+0

Nie byłem pewien co do kolejności pamięci w unlock_shared, ale moje rozumowanie polegało na tym, że tak naprawdę nie "wypuszcza" niczego, ponieważ ma dostęp tylko do odczytu i nie może zmienić danych, które chroni. – LWimsey

+0

@ LWimsey: tak, to jest trudniejsze myśleć o ładowaniu zamówień niż zamawianiu sklepu, ale to naprawdę. Obciążenie staje się globalnie widoczne w chwili odczytu danych z pamięci podręcznej L1. (Ponieważ wtedy robi się kopię ze spójnej globalnie pamięci podręcznej na rdzeń nieużywanego pojedynczego procesora.) –