Singleton

Appunti presi durante la lettura di Modern C++ Design: Generic Programming and Design Patterns Applied, di Andrei Alexandrescu.

Dal capitolo sei: Implementazione dei singleton.

Il pattern Singleton è semplice nella descrizione ma presenta alcune difficoltà non irrilevanti nella sua implementazione.

In pratica un singleton è una variabile globale migliorata, nel senso che non è possibile creare più di un oggetto di quel determinato tipo.

In genere le difficoltà vengono nel gestire il ciclo di vita di un singleton. È relativamente semplice controllarne la creazione, può diventare un incubo controllarne la distruzione.

Il singleton di Meyers

L'idea del singleton di Meyers è quella lasciare al compilatore l'onere di gestire il ciclo di vita dell'oggetto. Per far questo lo si definisce come variabile statica locale alla funzione che ne ritorna l'istanza. In questo modo l'oggetto viene effettivamente creato al momento del primo utilizzo e deallocato al termine dell'esecuzione del programma.

A seguire, un esempio che dovrebbe chiarire l'idea:

#include <iostream>

using namespace std;

class MeyersSingleton
{
public:
static MeyersSingleton& getInstance();
void doSomething();
private:
MeyersSingleton();
MeyersSingleton(const MeyersSingleton&); // not implemented
MeyersSingleton& operator=(const MeyersSingleton&); // not implemented
~MeyersSingleton();
};

MeyersSingleton& MeyersSingleton::getInstance()
{
static MeyersSingleton obj;
return obj;
}

MeyersSingleton::MeyersSingleton()
{
cout << "MeyersSingleton ctor" << endl;
}

MeyersSingleton::~MeyersSingleton()
{
cout << "MeyersSingleton dtor" << endl;
}

void MeyersSingleton::doSomething()
{
cout << "MeyersSingleton doSomething" << endl;
}

void tech60() {
cout << "get singleton I" << endl;
MeyersSingleton& ms = MeyersSingleton::getInstance();
ms.doSomething();

cout << "get singleton II" << endl;
MeyersSingleton& ms2 = MeyersSingleton::getInstance();
ms2.doSomething();

cout << "done testing singleton" << endl;
}

Una soluzione semplice ed elegante.

Le cose si complicano se vogliamo un maggiore controllo sul ciclo di vita del singleton, dato che bisogna gestire il problema della possibile morte dell'oggetto quando in realtà ne sarebbe ancora richiesto l'uso da qualche altra componente dell'applicazione. Un altra complicazione nasce dal fatto che il singleton potrebbe far uso di qualche risorsa che potrebbe non essere liberata propriamente al termine dell'applicazione. Il metodo indicato dall'Alexandrescu si basa sull'uso della funzione atexit(), che permette di registrare una funzione per l'esecuzione nella fase terminale del programma.

E una ulteriore complicazione ce la dà la gestione dei singleton in ambiente multithreading. In realtà, se seguiamo l'idea di Meyers possiamo non occuparci del problema, dato che lo deleghiamo al compilatore. Sarà lui a doversi fare carico di gestire possibili condizioni di racing al momento dell'effettiva generazione dell'oggetto. Ma se, per un motivo o per un altro, decidiamo di usare lo schema standard per la creazione di un singleton, mettendo l'oggetto creato nello heap, dobbiamo curare questo passaggio con attenzione.

La soluzione indicata da Alexandrescu è quella del pattern del lock con doppio controllo (double-checked locking pattern), con l'avvertenza di aggiungere un ulteriore controllo per verificare che l'hardware su cui gira la nostra applicazione lo supporti (ci consiglia di fare attenzione soprattutto in caso di ambienti multiprocessore simmetrico che implementino il cosiddetto modello di memoria rilassato).

In pratica usiamo un mutex, un lock, e controlliamo due volte che l'istanza dell'oggetto non sia stata creata. Lo scopo del doppio controllo è quello di evitare di utilizzare il lock appena sia possibile, dato che si tratta del collo di bottiglia prestazionale della vicenda.

Usando boost questa è la parte del codice rilevante:

#include <iostream>
#include <boost/thread/thread.hpp>
#include <boost/thread/mutex.hpp>

using namespace std;

namespace {
boost::mutex mtx;
}

class MultithreadedSingleton
{
public:
static MultithreadedSingleton& getInstance();
void doSomething();
private:
MultithreadedSingleton();
MultithreadedSingleton(const MultithreadedSingleton&); // not implemented
MultithreadedSingleton& operator=(const MultithreadedSingleton&); // not implemented
~MultithreadedSingleton();

static MultithreadedSingleton* pInstance_;
};

MultithreadedSingleton* MultithreadedSingleton::pInstance_ = 0;

MultithreadedSingleton& MultithreadedSingleton::getInstance()
{
if (pInstance_ == 0)
{
boost::mutex::scoped_lock lock(mtx);
if (pInstance_ == 0)
{
pInstance_ = new MultithreadedSingleton;
}
}

return *pInstance_;
}

// ...

Nessun commento:

Posta un commento