2012-11-20 4 views
7

Ho cercato modi per combinare un dato che sarà accessibile da più thread accanto al blocco fornito per la sicurezza del thread. Penso di essere arrivato a un punto in cui non penso sia possibile farlo mantenendo la costituttezza.Impossibile essere const-correct quando si combinano i dati e si blocca?

Prendere la seguente classe, ad esempio:

template <typename TType, typename TMutex> 
class basic_lockable_type 
{ 

public: 
    typedef TMutex lock_type; 

public: 
    template <typename... TArgs> 
    explicit basic_lockable_type(TArgs&&... args) 
     : TType(std::forward<TArgs...>(args)...) {} 

    TType& data() { return data_; } 
    const TType& data() const { return data_; } 

    void lock() { mutex_.lock(); } 
    void unlock() { mutex_.unlock(); } 

private: 
    TType   data_; 
    mutable TMutex mutex_; 

}; 

typedef basic_lockable_type<std::vector<int>, std::mutex> vector_with_lock; 

In questo cerco di combinare i dati e di blocco, la marcatura mutex_ come mutable. Purtroppo non è sufficiente perché lo vedo perché, se utilizzato, vector_with_lock deve essere contrassegnato come mutable per eseguire un'operazione di lettura da una funzione const che non è completamente corretta (data_ deve essere mutable da un const) .

void print_values() const 
{ 
    std::lock_guard<vector_with_lock> lock(values_); 
    for(const int val : values_) 
    { 
     std::cout << val << std::endl; 
    } 
} 

vector_with_lock values_; 

Chiunque può vedere in ogni caso intorno a questo in modo tale che const correttezza viene mantenuta, mentre la combinazione di dati e bloccare? Inoltre, ho fatto qualche ipotesi errata qui?

+2

Fare 'lock' e' unlock' const? (Inoltre, non dovrebbe essere 'std :: lock_guard '? Perché hai reso un nuovo lockable per se non hai intenzione di usarlo?) –

+0

Aggiornato 'lock_guard' – Graeme

+0

@ R.MartinhoFernandes Naturalmente, contrassegnando questi const consentirà una chiamata di istanza 'vector_with_lock' non mutabile' lock' e 'unlock', sì? – Graeme

risposta

6

Personalmente, preferirei un disegno in cui non è necessario bloccare manualmente e i dati sono incapsulati correttamente in modo che non si possa effettivamente accedervi senza bloccare prima.

Un'opzione è di avere una funzione amico apply o qualcosa che esegue il blocco, prende i dati incapsulati e li passa a un oggetto funzione che viene eseguito con il blocco trattenuto al suo interno.

//! Applies a function to the contents of a locker_box 
/*! Returns the function's result, if any */ 
template <typename Fun, typename T, typename BasicLockable> 
ResultOf<Fun(T&)> apply(Fun&& fun, locker_box<T, BasicLockable>& box) { 
    std::lock_guard<BasicLockable> lock(box.lock); 
    return std::forward<Fun>(fun)(box.data); 
} 
//! Applies a function to the contents of a locker_box 
/*! Returns the function's result, if any */ 
template <typename Fun, typename T, typename BasicLockable> 
ResultOf<Fun(T const&)> apply(Fun&& fun, locker_box<T, BasicLockable> const& box) { 
    std::lock_guard<BasicLockable> lock(box.lock); 
    return std::forward<Fun>(fun)(box.data); 
} 

Uso diventa allora:

void print_values() const 
{ 
    apply([](std::vector<int> const& the_vector) { 
     for(const int val : the_vector) { 
      std::cout << val << std::endl; 
     } 
    }, values_); 
} 

In alternativa, si può abusare gamma basata su ciclo for per correttamente la portata della serratura ed estrarre il valore come un'operazione di "single". Tutto ciò che è necessario è la corretta serie di iteratori :

for(auto&& the_vector : box.open()) { 
    // lock is held in this scope 
    // do our stuff normally 
    for(const int val : the_vector) { 
     std::cout << val << std::endl; 
    } 
} 

Penso che una spiegazione è in ordine. L'idea generale è che open() restituisca un handle RAII che acquisisce il blocco sulla costruzione e lo rilascia alla distruzione. Il ciclo for-based basato sulla gamma garantirà questa vita temporanea per tutto il tempo in cui il ciclo viene eseguito. Questo dà l'ambito di blocco appropriato.

Questo handle RAII fornisce anche gli iteratori begin() e end() per un intervallo con il valore singolo contenuto. Questo è il modo in cui possiamo ottenere i dati protetti. Il ciclo basato sulla gamma si occupa di fare il dereferenziazione per noi e di legarlo alla variabile di loop. Poiché l'intervallo è un singleton, il "loop" verrà eseguito sempre esattamente una volta.

Il box non dovrebbe fornire alcun altro modo per ottenere i dati, in modo che imponga effettivamente l'accesso interbloccato.

Ovviamente uno può riporre un riferimento ai dati una volta che la casella è aperta, in modo che il riferimento sia disponibile dopo che la casella si è chiusa. Ma questo è per proteggere contro Murphy, non contro Machiavelli.

Il costrutto sembra strano, quindi non darei la colpa a nessuno per non volerlo. Da una parte voglio usarlo perché la semantica è perfetta, ma d'altra parte non voglio perché non è quello per cui è basato il range. Sulla mano che stringe questa tecnica ibrida di gamma-RAII è piuttosto generica e può essere facilmente abusata per altri fini, ma la lascerò alla tua immaginazione/incubi;) Usa a tua discrezione.


lasciato come esercizio per il lettore, ma un breve esempio di una tale serie di iteratori può essere trovato nel mio locker_box implementation.

+0

O_o ​​che 'for' è piuttosto offensivo. – Mankarse

+0

E ho appena notato un bug nella mia implementazione: S Ooops. Non usare quel codice a casa, ragazzi. –

+0

È ancora bacato? Se sì, per quale motivo? – sehe

3

Che cosa si intende per "const correct"? In generale, penso che ci sia un consenso per la logica const, il che significa che se il mutex non fa parte dello stato logico (o osservabile) dell'oggetto, non c'è niente di sbagliato nel dichiararlo mutable e utilizzarlo anche nelle funzioni const .

0

In un certo senso, se il mutex è bloccato o meno fa parte dello stato osservabile dell'oggetto - è possibile osservarlo per esempio creando accidentalmente un'inversione di blocco.

Questo è un problema fondamentale con gli oggetti autobloccanti, e immagino che un aspetto di esso si riferisca alla cost-correttezza.

O è possibile modificare il "lockness" dell'oggetto tramite un riferimento-a-const, oppure non è possibile effettuare accessi sincronizzati tramite reference-to-const. Scegli uno, presumibilmente il primo.

L'alternativa è garantire che l'oggetto non possa essere "osservato" dal codice chiamante mentre si trova in uno stato bloccato, in modo che la chiusura non faccia parte dello stato osservabile. Ma non c'è modo per un chiamante di visitare ogni elemento nel loro vector_with_lock come una singola operazione sincronizzata. Non appena si chiama il codice dell'utente con il blocco trattenuto, è possibile scrivere un codice contenente un'inversione di blocco potenziale o garantita, che "vede" se il blocco è trattenuto o meno. Quindi per le raccolte questo non risolve il problema.

Problemi correlati