std::call_once vs std::mutex per l'inizializzazione thread-safe

std::call_once vs std::mutex per l'inizializzazione thread-safe


Sono un po' confuso sullo scopo di std::call_once . Per essere chiari, ho capito esattamente cosa std::call_once fa , e come usarlo. Di solito viene utilizzato per inizializzare atomicamente uno stato e assicurarsi che solo un thread inizializzi lo stato. Ho anche visto online molti tentativi di creare un singleton thread-safe con std::call_once .


Come dimostrato qui , supponiamo di scrivere un singleton thread-safe, in quanto tale:


CSingleton& CSingleton::GetInstance()
{
std::call_once(m_onceFlag, [] {
m_instance.reset(new CSingleton);
});
return *m_instance.get();
}

Ok, ho un'idea. Ma ho pensato che l'unica cosa std::call_once garantisce davvero che la funzione passata sarà solo essere eseguito una volta. Ma lo fa anche garantire che se c'è una gara per chiamare la funzione tra più thread e un thread vince, gli altri thread bloccheranno fino a quando il thread vincente non ritorna dalla chiamata?


Perché se è così, non vedo alcuna differenza tra call_once e un semplice mutex di sincronizzazione, come:


CSingleton& CSingleton::GetInstance()
{
std::unique_lock<std::mutex> lock(m_mutex);
if (!m_instance)
{
m_instance.reset(new CSingleton);
}
lock.unlock();
return *m_instance;
}

Quindi, se std::call_once in effetti costringe altri thread a bloccarsi, quindi quali vantaggi offre std::call_once offrire su un normale mutex? Pensandoci ancora un po', std::call_once certamente avrebbe per forzare il blocco degli altri thread o qualsiasi calcolo eseguito nella funzione fornita dall'utente non verrebbe sincronizzato. Quindi, di nuovo, cosa significa std::call_once offerta al di sopra di un normale mutex?


Risposte:


Una cosa che call_once fa per te è gestire le eccezioni. Cioè, se il primo thread al suo interno genera un'eccezione all'interno del functor (e la propaga all'esterno), call_once non prenderà in considerazione il call_once soddisfatto. Una successiva invocazione può inserire nuovamente il functor nel tentativo di completarlo senza eccezioni.


Nel tuo esempio, anche il caso eccezionale viene gestito correttamente. Tuttavia è facile immaginare un funtore più complicato in cui il caso eccezionale non sarebbe adeguatamente gestito.


Detto questo, prendo atto che call_once è ridondante con function-local-statics. Es.:


CSingleton& CSingleton::GetInstance()
{
static std::unique_ptr<CSingleton> m_instance(new CSingleton);
return *m_instance;
}

O più semplicemente:


CSingleton& CSingleton::GetInstance()
{
static CSingleton m_instance;
return m_instance;
}

Quanto sopra è equivalente al tuo esempio con call_once , e imho, più semplice. Oh, solo che l'ordine di distruzione è molto sottilmente diverso tra questo e il tuo esempio. In entrambi i casi m_instance viene distrutto in ordine inverso rispetto alla costruzione. Ma l'ordine di costruzione è diverso. Nel tuo m_instance è costruito rispetto ad altri oggetti con ambito file-local nella stessa unità di traduzione. Usando function-local-statics, m_instance viene costruito la prima volta GetInstance viene eseguito.


Tale differenza può o non può essere importante per la tua applicazione. In genere preferisco la soluzione function-local-static in quanto è "pigra". Cioè. se l'applicazione non chiama mai GetInstance() quindi m_instance non è mai costruito. E non c'è alcun periodo durante l'avvio dell'applicazione in cui molti elementi statici stanno cercando di essere costruiti contemporaneamente. Paghi per la costruzione solo quando effettivamente utilizzata.


Alcune risposte al codice


CSingleton&
CSingleton::GetInstance() {
std::call_once(m_onceFlag, [] {
m_instance.reset(new CSingleton);
});
return *m_instance.get();
}
CSingleton&
CSingleton::GetInstance() {
std::unique_lock<std::mutex>
lock(m_mutex);
if (!m_instance)
{
m_instance.reset(new CSingleton);
}
lock.unlock();
return *m_instance;
}
CSingleton&
CSingleton::GetInstance() {
static std::unique_ptr<CSingleton>
m_instance(new CSingleton);
return *m_instance;
}
CSingleton&
CSingleton::GetInstance() {
static CSingleton m_instance;
return m_instance;
}
// header.h namespace dbj_once {
struct singleton final {};
inline singleton &
instance()
{
static singleton single_instance = []() ->
singleton { // this is called only once // do some more complex initialization // here return {};
}();
return single_instance;
};
} // dbj_once
#include <thread>
#include <mutex>
static std::once_flag flag;
void f(){
operation_that_takes_time();
std::call_once(flag, [](){std::cout <<
"f() was called\n";});
} void g(){
operation_that_takes_time();
std::call_once(flag, [](){std::cout <<
"g() was called\n";});
} int main(int argc, char *argv[]){
std::thread t1(f);
std::thread t2(g);
t1.join();
t2.join();
}