2013-05-15 14 views
5

Ho un codice che deve essere thread-safe ed eccezionalmente sicuro. Il codice che segue è una versione molto semplificata del mio problema:Blocco di un mutex in un distruttore in 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;} 

Ho un mutex che ho bisogno di bloccare il distruttore di un oggetto. Il problema è che il mio distruttore non dovrebbe lanciare eccezioni.

Cosa posso fare?

0) Non posso sostituire n con una variabile atomica (naturalmente sarebbe fare il trucco qui, ma non è questo il punto della mia domanda)

1) ho potuto sostituire il mio mutex con una rotazione di blocco

2) Potrei provare a catturare il blocco in un ciclo infinito finché non acquisirò il blocco senza eccezione sollevata

Nessuna di queste soluzioni sembra molto allettante. Hai avuto lo stesso problema? Come l'hai risolto ?

+3

'Ho un mutex che ho bisogno di bloccare il distruttore di un oggetto' - Sembra una cattiva idea. Invece di fornirci la soluzione e averci risolto i problemi, forse dovresti dirci quale problema stai cercando di risolvere, in modo che possiamo fornire una soluzione migliore. –

+1

@RobertHarvey Quello che voglio veramente fare è inserire le modifiche in una cache condivisa dopo che sono state salvate in un database. – Arnaud

risposta

8

Come suggerito da Adam H. Peterson, ho finalmente deciso di scrivere un mutex nessun tiro:

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);} 

L'idea è quella di avere due mutex, invece di uno solo. Il vero mutex è il mutex di spin implementato con un std::atomic_flag. Questo spin mutex è protetto da un std::mutex che potrebbe lanciare.

In una situazione normale, viene acquisito il mutex standard e il flag viene impostato con un costo di una sola operazione atomica. Se il mutex standard non può essere bloccato immediatamente, il thread sta andando a dormire.

Se per qualsiasi motivo il mutex standard getta, il mutex entrerà nella sua modalità di rotazione. Il thread in cui si è verificata l'eccezione verrà quindi loop finché non sarà possibile impostare il flag. Dato che nessun altro thread è consapevole del fatto che questa discussione abbia completamente assorbito il mutex standard, potrebbero girare anche loro.

Nel peggiore dei casi, questo meccanismo di blocco si riduce a un blocco di rotazione. Il più delle volte reagisce come un normale mutex.

3

Questa è una brutta situazione. Il distruttore sta facendo qualcosa che potrebbe fallire. Se l'errore di aggiornare questo contatore non corroderà irrimediabilmente la tua applicazione, potresti semplicemente lasciare che il distruttore lanci. Questo causerà il crash dell'applicazione con una chiamata a terminate, ma se l'applicazione è corrotta, potrebbe essere meglio uccidere il processo e fare affidamento su qualche schema di ripristino di livello superiore (come un watchdog per un demone o riprovare l'esecuzione per un'altra utilità) . Se il fallimento di diminuire il contatore è recuperabile, si dovrebbe assorbire l'eccezione con un blocco try{}catch() e ripristinare (o potenzialmente salvare le informazioni per qualche altra operazione per recuperare eventualmente). Se non è ripristinabile, ma non è fatale, potresti voler catturare e assorbire l'eccezione e registrare l'errore (assicurandosi di accedere in modo sicuro alle eccezioni, ovviamente).

Sarebbe ideale se il codice potesse essere ristrutturato in modo tale che il distruttore non facesse nulla che non potesse fallire. Tuttavia, se il tuo codice è corretto in modo corretto, il fallimento durante l'acquisizione di un blocco è probabilmente raro, tranne che nei casi di risorse limitate, quindi l'assorbimento o l'interruzione del fallimento potrebbe essere accettabile. Per alcuni mutex, lock() è probabilmente un'operazione di non lancio (come uno spinlock che utilizza atomic_flag), e se è possibile utilizzare tale mutex, è possibile aspettarsi che lock_guard non venga mai lanciato. La tua unica preoccupazione in quella situazione sarebbe la situazione di stallo.