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.
Meglio in che senso? Cosa non ti piace di questo approccio? –
@DavidSchwartz Sta bloccando il suo thread GUI attraverso il suo costoso distruttore. Presumo che voglia evitare questo. –
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. –