2013-05-15 34 views
5

Mam kod, który musi być bezpieczny dla wątków i wyjątek bezpieczny. Poniższy kod jest bardzo uproszczoną wersją mojego problemu:Blokowanie muteksu w destruktorze w C++ 11

#include <mutex> 
#include <thread> 

std::mutex mutex; 
int n=0; 

class Counter{ 
public: 
    Counter(){ 
     std::lock_guard<std::mutex>guard(mutex); 
     n++;} 
    ~Counter(){ 
     std::lock_guard<std::mutex>guard(mutex);//How can I protect here the underlying code to mutex.lock() ? 
     n--;} 
}; 

void doSomething(){ 
    Counter counter; 
    //Here I could do something meaningful 
} 

int numberOfThreadInDoSomething(){ 
    std::lock_guard<std::mutex>guard(mutex); 
    return n;} 

Mam muteks, który muszę zablokować w destruktorze obiektu. Problem polega na tym, że mój destruktor nie powinien generować wyjątków.

Co mogę zrobić?

0) Nie mogę zastąpić n ze zmienną atomowej (oczywiście byłoby załatwić sprawę tutaj, ale to nie o to chodzi w moim pytaniu)

1) mogę wymienić mój mutex z korkociągu zablokować

2) Mogę spróbować złapać blokadę w nieskończoną pętlę, aż ostatecznie zdobędę blokadę bez wyjątku podniesionego.

Żadne z tych rozwiązań nie wydaje się bardzo atrakcyjne. Czy masz ten sam problem? Jak to rozwiązałeś?

+3

"Mam muteks, który muszę zablokować w destruktorze obiektu" - brzmi jak zły pomysł. Zamiast dostarczania nam rozwiązania i usuwania z nim problemów, może powinieneś nam powiedzieć, jaki problem próbujesz rozwiązać, abyśmy mogli zapewnić lepsze rozwiązanie. –

+1

@RobertHarvey Co naprawdę chcę zrobić, to wstawianie modyfikacji do współużytkowanej pamięci podręcznej po ich zapisaniu w bazie danych. – Arnaud

Odpowiedz

8

Jak sugeruje Adam H. Peterson, ale w końcu postanowiłem napisać mutex nie Rzut obronny:

class NoThrowMutex{ 
private: 
    std::mutex mutex; 
    std::atomic_flag flag; 
    bool both; 
public: 
    NoThrowMutex(); 
    ~NoThrowMutex(); 
    void lock(); 
    void unlock(); 
}; 

NoThrowMutex::NoThrowMutex():mutex(),flag(),both(false){ 
    flag.clear(std::memory_order_release);} 

NoThrowMutex::~NoThrowMutex(){} 

void NoThrowMutex::lock(){ 
    try{ 
     mutex.lock(); 
     while(flag.test_and_set(std::memory_order_acquire)); 
     both=true;} 
    catch(...){ 
     while(flag.test_and_set(std::memory_order_acquire)); 
     both=false;}} 

void NoThrowMutex::unlock(){ 
    if(both){mutex.unlock();} 
    flag.clear(std::memory_order_release);} 

Chodzi o to, aby mieć dwa muteksu zamiast tylko jednego. Prawdziwy muteks to spin mutex zaimplementowany za pomocą std::atomic_flag. Ten mutex spin jest chroniony przez std::mutex, który może rzucić.

W normalnej sytuacji nabywany jest standardowy muteks, a flaga jest ustawiona kosztem tylko jednej operacji atomowej. Jeśli standardowy muteks nie może zostać natychmiast zablokowany, wątek przejdzie w stan uśpienia.

Jeśli z jakiegokolwiek powodu zostanie wyrzucony standardowy muteks, muteks wejdzie w tryb wirowania. Wątek, w którym wystąpił wyjątek, zostanie zapętlony, aż ustawi flagę. Żaden inny wątek nie jest świadomy tego, że ten wątek całkowicie odwołuje się do standardowego muteksa, może też się obracać.

W najgorszym przypadku ten mechanizm blokowania pogarsza się do blokady spinu. Przez większość czasu reaguje tak, jak normalny muteks.

3

To jest zła sytuacja. Twój destruktor robi coś, co może się nie udać. Jeśli brak aktualizacji tego licznika spowoduje nieodwracalne uszkodzenie aplikacji, możesz po prostu pozwolić rzucić destruktora. Spowoduje to awarię twojej aplikacji wywołaniem terminate, ale jeśli twoja aplikacja jest uszkodzona, może lepiej zabić proces i polegać na schemacie odzyskiwania wyższego poziomu (takim jak watchdog dla demona lub wykonanie dla innego narzędzia) . Jeśli nie można zmniejszyć licznika, można odzyskać, powinieneś zaabsorbować wyjątek z blokiem try{}catch() i odzyskać (lub potencjalnie zapisać informacje dla jakiejś innej operacji, aby ostatecznie odzyskać). Jeśli nie można go odzyskać, ale nie jest to krytyczne, możesz złapać i zaabsorbować wyjątek i zarejestrować awarię (oczywiście logując się w sposób bezpieczny dla wyjątków).

Byłoby idealnie, gdyby kod mógł zostać zrestrukturyzowany w taki sposób, że destruktor nie zrobi niczego, co nie może zawieść. Jeśli jednak Twój kod jest poprawny, niepowodzenie podczas uzyskiwania blokady jest prawdopodobnie rzadkie, z wyjątkiem przypadków ograniczonych zasobów, więc zarówno absorpcja, jak i przerwanie w przypadku awarii może być z powodzeniem akceptowalne. W przypadku niektórych muteksów, lock() jest prawdopodobnie operacją no-throw (taką jak spinlock używająca atomic_flag), a jeśli możesz użyć takiego mutex, możesz oczekiwać, że lock_guard nigdy nie rzuci. Waszym jedynym zmartwieniem w tej sytuacji byłby impas.