2015-09-29 10 views
9

Ho una classe simile a:sicuro rilasciare una risorsa su un thread diverso

class A{ 
    private: 
    boost::shared_ptr<Foo> m_pFoo; 
} 

istanze di un vengono distrutte sul thread GUI dove possono tenere l'ultimo riferimento a un Foo. Il distruttore per Foo è potenzialmente di lunga durata, causando una pausa indesiderata sul mio thread GUI. Mi piacerebbe che Foo fosse distrutto su un thread separato in questo caso, Foo's è autonomo e non è critico viene rilasciato immediatamente.

Attualmente, utilizzare un modello come questo:

A::~A(){ 
    auto pMtx = boost::make_shared<boost::mutex>(); 
    boost::unique_lock<boost::mutex> destroyerGate(*pMtx); 
    auto pFoo = m_pFoo; 
    auto destroyer = [pMtx,pFoo](){ 
     boost::unique_lock<boost::mutex> gate(*pMtx); 
    }; 

    m_pFoo.reset(); 
    pFoo.reset(); 
    s_cleanupThread->post(destroyer); 
} 

Essenzialmente, catturare in un lambda e bloccare fino al rilascio dall'oggetto. C'è un modo migliore per realizzare questo? Questo sembra solo più complicato di quanto dovrebbe essere.

+0

Meglio in che senso? Cosa non ti piace di questo approccio? –

+0

@DavidSchwartz Sta bloccando il suo thread GUI attraverso il suo costoso distruttore. Presumo che voglia evitare questo. –

+11

Invece di girare un thread per distruggere ogni oggetto, potresti avere un thread destroyer che prende "shared_ptr's da una coda e blocca quando la coda è vuota. Non so se avrebbe dei vantaggi. –

risposta

0

A non dovrebbe essere responsabile della distruzione dell'obiettivo di m_pFoo. Distruzione della risorsa a cui i punti shared_ptr sono responsabili dello shared_ptr, quindi nella mia mente non si deve gestire in modo microscopico il thread su cui si verifica la distruzione dell'oggetto reale all'interno di ~A.

Invece di implementare un nuovo tipo di puntatore intelligente che si adatta alle tue esigenze, penso che un buon compromesso qui sia quello di prendere la logica relativa alla cancellazione dell'oggetto sottostante da ~A e spostarlo in un deleter personalizzato che fornisci allo shared_ptr in corso di costruzione. Se sei soddisfatto della tua attuale strategia di deallocation, penso che questo sia un approccio soddisfacente. Ma sono anche d'accordo con gli altri sul fatto che potresti voler indagare su strategie che non implicano la creazione di un nuovo thread per ogni deallocazione.

È possibile trovare i documenti su come fornire un deleter allo smart_ptrhere. Scorri verso il basso fino a "Costruttori che eseguono un deleter" (potresti anche voler consultare la documentazione per la versione specifica di boost che stai utilizzando).

+0

Un oggetto è quello che è, e se una classe proprietaria non apprezza il comportamento, allora chi è responsabile della modifica di quel comportamento? Penso alla tua destra; il distruttore di classe A non dovrebbe preoccuparsi di questo, specialmente se la classe Foo è un'altra parte del codice sorgente dell'applicazione. Se Foo proviene da una libreria di classi non modificabile e opaca, allora raggirare in questo modo nella classe A sarebbe giustificato? – bazza

+0

Ho detto che la deallocazione dell'oggetto sottostante è responsabilità di 'shared_ptr'. Non sto suggerendo che un buon posto per la correzione sia all'interno di "Foo". Il decostruttore di 'Foo' non si preoccupa del CONTESTO in cui è chiamato, ma incapsula semplicemente la distruzione di un' Foo'. 'shared_ptr' è il tipo che sta facendo asserzione sul contesto in cui' Foo' sarà distrutto. In questo caso, dice implicitamente che la distruzione si verifica sullo stesso thread. Quindi la mia soluzione proposta è scrivere un puntatore intelligente personalizzato o fornire un deleter personalizzato a 'shared_ptr'. –

+0

Ah, ok, scusa ho frainteso. Propongo provvisoriamente che l'interfaccia presentata da una classe dovrebbe includere le sue caratteristiche di distruzione così come i suoi membri pubblici. Se c'è un requisito per specifiche caratteristiche di distruzione (come la distruzione rapida), allora la classe dovrebbe consegnarlo. In caso contrario, il comportamento è parzialmente implementato nella classe, in parte nel suo proprietario. Mi sembra sbagliato. A mio parere mettere la parte difficile nel distruttore di Foo è probabilmente il posto giusto. – bazza

0

Come già suggerito nei commenti, Mark Ransom, è possibile utilizzare un thread di distruzione dedicato che preleva gli oggetti da distruggere da una coda di lavoro e quindi li lascia semplicemente sul pavimento. Questo funziona partendo dal presupposto che se ti allontani da un oggetto, la distruzione dell'oggetto spostato via sarà molto economico.

Qui sto proponendo una classe destruction_service, templata sul tipo di oggetto che si desidera distruggere. Questo può essere qualsiasi tipo di oggetto, non solo indicatori condivisi. In effetti, i puntatori condivisi sono anche quelli più difficili perché devi stare attento, devi solo inviare il std::shared_ptr per la distruzione se il suo numero di riferimenti ha raggiunto uno. Altrimenti, la distruzione del std::shared_ptr sul thread di distruzione sarà fondamentalmente un no-op, tranne per il decremento del conteggio dei riferimenti. In questo caso, però, non succederà nulla di veramente brutto. Finirai per distruggere l'oggetto su un thread che non avrebbe dovuto farlo e quindi potrebbe essere bloccato più a lungo dell'ideale. Per il debug, nel tuo distruttore potresti assert che non sei nella discussione principale.

Avrò bisogno che il tipo abbia un distruttore di non lancio e spostare l'operatore di costruzione, comunque.

A destruction_service<T> mantiene uno std::vector<T> degli oggetti che devono essere distrutti. Invio di un oggetto per distruzione push_back() s su quel vettore.Il thread di lavoro attende che la coda diventi non vuota e quindi swap() s con il proprio numero vuoto std::vector<T>. Dopo aver lasciato la sezione critica, è il clear() s il vettore che distrugge tutti gli oggetti. Il vettore stesso viene tenuto in giro in modo che possa essere swap() indietro la volta successiva riducendo la necessità di allocazioni di memoria dinamica. Se sei preoccupato che i std::vector non si riducano mai, considera invece l'uso di std::deque. Mi asterrò dall'usare uno std::list perché assegna la memoria per ogni oggetto ed è un po 'paradossale allocare memoria per distruggere un oggetto. Il solito vantaggio dell'utilizzo di std::list come coda di lavoro è che non è necessario allocare memoria nella sezione critica ma distruggere gli oggetti è probabilmente un'attività a bassa priorità e non mi interessa se il thread di lavoro è bloccato un po 'più a lungo del necessario finché il thread principale rimane reattivo. Non esiste un modo standard per impostare la priorità di un thread in C++ ma, se lo si desidera, è possibile provare a dare al thread worker una priorità bassa tramite native_handle (nel costruttore di destruction_service), dato che la piattaforma lo consente.

Il distruttore di destruction_service sostituirà join() il thread di lavoro. Come scritto, la classe non è copiabile e non mobile. Mettilo in un puntatore intelligente se hai bisogno di muoverlo.

#include <cassert>    // assert 
#include <condition_variable> // std::condition_variable 
#include <mutex>    // std::mutex, std::lock_guard, std::unique_lock 
#include <thread>    // std::thread 
#include <type_traits>   // std::is_nothrow_{move_constructible,destructible} 
#include <utility>    // std::move 
#include <vector>    // std::vector 


template <typename T> 
class destruction_service final 
{ 

    static_assert(std::is_nothrow_move_constructible<T>::value, 
       "The to-be-destructed object needs a non-throwing move" 
       " constructor or it cannot be safely delivered to the" 
       " destruction thread"); 

    static_assert(std::is_nothrow_destructible<T>::value, 
       "I'm a destruction service, not an ammunition disposal" 
       " facility"); 

public: 

    using object_type = T; 

private: 

    // Worker thread destroying objects. 
    std::thread worker_ {}; 

    // Mutex to protect the object queue. 
    mutable std::mutex mutex_ {}; 

    // Condition variable to signal changes to the object queue. 
    mutable std::condition_variable condvar_ {}; 

    // Object queue of to-be-destructed items. 
    std::vector<object_type> queue_ {}; 

    // Indicator that no more objects will be scheduled for destruction. 
    bool done_ {}; 

public: 

    destruction_service() 
    { 
    this->worker_ = std::thread {&destruction_service::do_work_, this}; 
    } 

    ~destruction_service() noexcept 
    { 
    { 
     const std::lock_guard<std::mutex> guard {this->mutex_}; 
     this->done_ = true; 
    } 
    this->condvar_.notify_all(); 
    if (this->worker_.joinable()) 
     this->worker_.join(); 
    assert(this->queue_.empty()); 
    } 

    void 
    schedule_destruction(object_type&& object) 
    { 
    { 
     const std::lock_guard<std::mutex> guard {this->mutex_}; 
     this->queue_.push_back(std::move(object)); 
    } 
    this->condvar_.notify_all(); 
    } 

private: 

    void 
    do_work_() 
    { 
    auto things = std::vector<object_type> {}; 
    while (true) 
     { 
     { 
      auto lck = std::unique_lock<std::mutex> {this->mutex_}; 
      if (this->done_) 
      break; 
      this->condvar_.wait(lck, [this](){ return !queue_.empty() || done_; }); 
      this->queue_.swap(things); 
     } 
     things.clear(); 
     } 
    // By now, we may safely modify `queue_` without holding a lock. 
    this->queue_.clear(); 
    } 

}; 

Ecco un semplice caso d'uso:

#include <atomic> // std::atomic_int 
#include <thread> // std::this_thread::{get_id,yield} 
#include <utility> // std::exchange 

#include "destruction_service.hxx" 


namespace /* anonymous */ 
{ 

    std::atomic_int example_count {}; 
    std::thread::id main_thread_id {}; 

    class example 
    { 

    private: 

    int id_ {-1}; 

    public: 

    example() : id_ {example_count.fetch_add(1)} 
    { 
     std::this_thread::yield(); 
    } 

    example(const example& other) : id_ {other.id_} 
    { 
    } 

    example(example&& other) noexcept : id_ {std::exchange(other.id_, -1)} 
    { 
    } 

    ~example() noexcept 
    { 
     assert(this->id_ < 0 || std::this_thread::get_id() != main_thread_id); 
     std::this_thread::yield(); 
    } 

    }; 

} // namespace /* anonymous */ 


int 
main() 
{ 
    main_thread_id = std::this_thread::get_id(); 
    destruction_service<example> destructor {}; 
    for (int i = 0; i < 12; ++i) 
    { 
     auto thing = example {}; 
     destructor.schedule_destruction(std::move(thing)); 
    } 
} 

Grazie a Barry per reviewing this code e fare alcuni buoni suggerimenti per migliorarlo. Si prega di vedere my question on Code Review per una versione ridotta del codice ma senza i suoi suggerimenti incorporati.

Problemi correlati