2012-10-23 12 views
12

To pytanie zadano w wywiadzie i nie mogłem na nie odpowiedzieć.Jak zaimplementować atomowy (wątkowy) i bezpieczny dla wyjątków operator przypisania kopii?

Dokładniej, klasę, do której należy operator przypisania wygląda następująco:

class A { 
private: 
    B* pb; 
    C* pc; 
    .... 
public: 
    .... 
} 

Jak wdrożyć atomową (thread-safe) i wyjątku bezpieczne, głęboko kopiowania operator przypisania dla ta klasa?

+5

operatora przypisania wątku bezpieczny? te osoby przeprowadzające ankietę naprawdę ją przesuwają. – stijn

+1

Wyjątek bezpieczny w jakim stopniu? –

+2

Jak wdrożyć wszystko - wszystko bezpieczne? Z pewnością nie za pomocą zwykłych wskaźników C i rozmyślania z kopiami przedmiotów, które wskazują ... mamy RAI i inteligentne wskaźniki dla bezpieczeństwa, prawda? – leftaroundabout

Odpowiedz

12

Istnieją dwie odrębne kwestie (bezpieczeństwo wątków i wyjątki - bezpieczeństwo) i najlepiej jest rozwiązać je osobno. Aby umożliwić konstruktorom wzięcie innego obiektu jako argumentu, aby uzyskać blokadę podczas inicjowania elementów, konieczne jest w ogóle faktorowanie elementów danych w oddzielnej klasie: W ten sposób można uzyskać blokadę podczas inicjowania obiektu podrzędnego i klasy utrzymującej rzeczywiste dane może zignorować wszelkie problemy z współbieżnością. W ten sposób klasa zostanie podzielona na dwie części: class A, aby poradzić sobie z problemami współbieżności i class A_unlocked, aby zachować dane. Ponieważ funkcje członkowskie A_unlocked nie mają żadnej ochrony przed współbieżnością, nie powinny być bezpośrednio widoczne w interfejsie, a zatem A_unlocked jest prywatnym członkiem A.

Tworzenie bezpiecznego dla wyjątków operatora przypisania przebiega prosto, wykorzystując konstruktora kopiowania. Argument jest kopiowany i członkowie są zamienione:

A_unlocked& A_unlocked::operator= (A_unlocked const& other) { 
    A_unlocked(other).swap(*this); 
    return *this; 
} 

Oczywiście oznacza to, że odpowiedni konstruktor kopiujący i członkiem swap() są realizowane. Radzenie sobie z alokacją wielu zasobów, np. Wieloma obiektami przydzielonymi na stercie, jest najłatwiejsze dzięki posiadaniu odpowiedniej procedury obsługi zasobów dla każdego z obiektów. Bez użycia funkcji obsługi zasobów bardzo szybko staje się kłopotliwe, aby poprawnie usunąć wszystkie zasoby w przypadku wystąpienia wyjątku. W celu utrzymania przydzielonej pamięci sterty std::unique_ptr<T> (lub std::auto_ptr<T>, jeśli nie możesz używać C++ 2011) jest odpowiednim wyborem. Poniższy kod kopiuje tylko wskazane obiekty, chociaż nie ma większego sensu przydzielanie obiektów na stercie, niż przypisywanie ich członkom. W prawdziwej przykład obiekty prawdopodobnie wdrożyć metodę clone() lub inny mechanizm do tworzenia obiektu odpowiedniego typu:

class A_unlocked { 
private: 
    std::unique_ptr<B> pb; 
    std::unique_ptr<C> pc; 
    // ... 
public: 
    A_unlocked(/*...*/); 
    A_unlocked(A_unlocked const& other); 
    A_unlocked& operator= (A_unlocked const& other); 
    void swap(A_unlocked& other); 
    // ... 
}; 

A_unlocked::A_unlocked(A_unlocked const& other) 
    : pb(new B(*other.pb)) 
    , pc(new C(*other.pc)) 
{ 
} 
void A_unlocked::swap(A_unlocked& other) { 
    using std::swap; 
    swap(this->pb, other.pb); 
    swap(this->pc, other.pc); 
} 

Dla kawałka nitki bezpieczeństwa konieczne jest wiedzieć, że żaden inny wątek jest niepożądanym skopiowany obiekt. Aby to zrobić, użyj muteksu. Oznacza to, że class A wygląda mniej więcej tak:

class A { 
private: 
    mutable std::mutex d_mutex; 
    A_unlocked   d_data; 
public: 
    A(/*...*/); 
    A(A const& other); 
    A& operator= (A const& other); 
    // ... 
}; 

uwaga, że ​​wszyscy członkowie A będzie trzeba zrobić pewną ochronę współbieżności jeśli obiekty typu A mają być wykorzystywane bez blokowania zewnętrznego.Ponieważ muteks używany do ochrony przed współbieżnym dostępem nie jest tak naprawdę częścią stanu obiektu, ale musi zostać zmieniony nawet podczas odczytu stanu obiektu, jest on tworzony jako mutable. Mając to na miejscu, tworząc konstruktor kopiujący jest prosta:

A::A(A const& other) 
    : d_data((std::unique_lock<std::mutex>(other.d_mutex), other.d_data)) { 
} 

To blokuje mutex i delegatów argument do konstruktora kopii członka. Blokada jest automatycznie zwalniana na końcu wyrażenia, niezależnie od tego, czy kopia się powiodła, czy też rzuciła wyjątek. Konstruowany obiekt nie potrzebuje żadnego blokowania, ponieważ nie ma jeszcze sposobu, aby inny wątek wiedział o tym obiekcie.

Główna logika operatora przypisania również po prostu przekazuje do bazy, używając operatora przypisania. Najtrudniejsze jest to, że istnieją dwa muteksy, które muszą być zablokowane: jeden dla przypisanego obiektu i jeden dla argumentu. Ponieważ inny wątek może przypisać dwa obiekty w dokładnie odwrotny sposób, istnieje możliwość zablokowania. Dogodnie standardowa biblioteka C++ zapewnia algorytm std::lock(), który nabywa blokady w odpowiedni sposób, unikając ślepych blokad. Jednym ze sposobów wykorzystania tego algorytmu jest przekazanie w odblokowanych std::unique_lock<std::mutex> przedmiotów, po jednym dla każdej mutex potrzebnego do nabycia:

A& A::operator= (A const& other) { 
    if (this != &other) { 
     std::unique_lock<std::mutex> guard_this(this->d_mutex, std::defer_lock); 
     std::unique_lock<std::mutex> guard_other(other.d_mutex, std::defer_lock); 
     std::lock(guard_this, guard_other); 

     *this->d_data = other.d_data; 
    } 
    return *this; 
} 

Jeśli w dowolnym momencie cesji jest wyjątek, strażnicy zamek wyda muteksy i moduły obsługi zasobów zwolnią wszystkie nowo przydzielone zasoby. Tak więc powyższe podejście wprowadza silną gwarancję wyjątku. Co ciekawe, przypisanie kopii musi wykonać test samoporządkowania, aby zapobiec dwukrotnemu blokowaniu tego samego muteksu. Zwykle utrzymuję, że niezbędna kontrola własna jest oznaką, że operator przypisania nie jest bezpieczny w wyjątkach, ale myślę, że powyższy kod jest wyjątkowo bezpieczny.

To poważna przeróbka odpowiedzi. Wcześniejsze wersje tej odpowiedzi były albo podatne na utratę aktualizacji, albo na awaryjną blokadę. Dzięki Yakk za wskazanie problemów. Chociaż w wyniku zajęcia się problemem wymaga więcej kodu, myślę, że każda poszczególna część kodu jest w rzeczywistości prostsza i może być zbadana pod kątem poprawności.

+0

nie doprowadziłoby to do tego, że przestaniesz być bezpieczny dla wątków (wątek A, wątek b modyfikuje)? Czy nie powinno się zablokować obu i zrobić specjalny przypadek, jeśli źródło i cel są takie same, a następnie tylko jeden zamek? Poza tym, w jaki sposób selfAssignments ma sens, czy istnieje przykład? – ted

+2

Kopiowanie prawej strony ma charakter atomowy, ponieważ jest zablokowane podczas wykonywania kopii. Przypisanie na lewą stronę jest legalne, dopóki nie zostanie podjęta blokada, co jest koncepcyjnie równoznaczne z podjęciem go przed kopiowaniem, z wyjątkiem przypadku samodzielnego przypisania. Istnieje możliwość samodzielnego przypisania wykonania kopii, a inny wątek powoduje odwijanie obiektu przed jego umieszczeniem. Jest to interesujące miejsce, w którym sprawdzenie samodzielności może być uzasadnione. Samotrzymanie nie powinno się zdarzyć, ale jeśli się stanie, nie powinno to powodować niepowodzenia programów. –

+0

To wygląda solidnie. Dodam tylko, że w zależności od implementacji wspomnianego egzemplarza ctor, to rozwiązanie (prawdopodobnie większość/wszystkie praktyczne rozwiązania?) Może w najlepszym przypadku zaoferować [silną gwarancję wyjątku] (http://en.wikipedia.org/wiki/Exception_guarantees) ponieważ prawdopodobnie dynamicznie przydzieli kopie B, C. Ponadto, należy zachować ostrożność, aby nie pozostawić muteksu zablokowanego w przypadku wyjątku, jeśli rzeczywiście chce on uzyskać mocną gwarancję. – WeirdlyCheezy

0

Wyjątek bezpieczny? Operacje na elementach prymitywnych nie są rzucane, więc możemy uzyskać to za darmo.

Atomowy? Najprostszy byłby zamiennik atomowy dla 2x sizeof(void*) - Wierzę, że większość platform to oferuje. Jeśli tego nie zrobisz, będziesz musiał użyć blokady, albo istnieją algorytmy bez blokady, które mogą działać.

Edytuj: Głębokie kopiowanie, co? Musisz skopiować A i B na nowe tymczasowe inteligentne wskaźniki, a następnie zamienić je atomicznie.

+0

Czy mógłbyś dodać kilka linii kodów? –

+4

Nie. Dlaczego miałbym to robić? – Puppy

4

Po pierwsze, musisz zrozumieć, że żadna operacja nie jest bezpieczna dla wątków, ale raczej wszystkie operacje na danym zasobie mogą być wzajemnie bezpieczne. Musimy więc omówić zachowanie kodu operatora nieprzypisującego.

Najprostszym rozwiązaniem byłoby uczynienie danych niezmiennymi, napisanie klasy Aref, która używa klasy pImpl do przechowywania niezmiennego odnośnika zliczonego A, i posiadające metody mutacji na Aref, powodujące utworzenie nowego A. Możesz osiągnąć ziarnistość, mając niezmienne referencyjne liczone składniki A (jak B i C), stosując podobny wzór. Zasadniczo, Aref staje się opakowaniem COW (copy on write) pImpl dla A (możesz włączyć optymalizacje do obsługi pojedynczych przypadków, aby pozbyć się zbędnej kopii).

Drugą drogą byłoby stworzenie monolitycznego zamka (mutex lub czytnik-czytnik) na A i wszystkich jego danych. W takim przypadku potrzebujesz albo zamawiania muteksa na blokadach dla instancji A (albo podobnej techniki), aby stworzyć operatora bez wyścigu =, albo zaakceptować możliwie zaskakującą możliwość warunków wyścigu i wspomnij o idiomie wymiany kopii, o którym wspomniał Dietmar. (Kopiowanie-ruch jest również dopuszczalne) (Wyraźny stan wyścigu w operacji kopiowania-kopiowania, operator przypisania zamka blokady =: Wątek 1 ma wartość X = Y. Wątek 2 ma wartość Y.flag = true, X.flag = true. Następnie należy podać: X .flag jest fałszywe, nawet jeśli Thread2 blokuje X i Y w całym zadaniu, może się to zdarzyć, co zaskoczy wielu programistów.)

W pierwszym przypadku kod nieprzypisania musi być zgodny z kopią pisz semantykę. W drugim przypadku kod nieprzypisania musi być zgodny z blokadą monolityczną.

Jeśli chodzi o bezpieczeństwo wyjątków, zakładając, że konstruktor kopii jest bezpieczny pod względem wyjątków, podobnie jak kod blokady, blokada blokady-zamka (druga) jest wyjątkowo bezpieczna. W przypadku pierwszego, tak długo jak liczenie referencji, blokada klonu i kod modyfikacji danych jest wyjątkowo bezpieczny, jesteś dobry: operator = kod jest w obu przypadkach całkiem martwy. (Upewnij się, że zamki są RAII, przechowywać wszystkie przydzielonej pamięci w uchwycie wskaźnik std RAII (z możliwością uwolnienia jeśli kończy się oddaniem go wyłączyć), itd.)

Powiązane problemy