Poesía Binaria

Singletons en C++ y alguna nota sobre thread safety (I)

Antes de nada, comentar que he dividido este post en dos porque vi que se estaba alargando demasiado y se lanzarán uno al día, pondré aquí enlaces a todos los posts.

Muchas veces cuando estamos programando tenemos la necesidad de crear un objeto de una clase determinada, pero éste objeto deberá ser creado una sola vez en nuestra aplicación y debemos evitar a toda costa que pueda ser creado más veces. Podemos pensar en:

También es cierto que para muchos casos el uso del patrón Singleton no es obligatorio, en otros casos no tenemos que usarlo y mucha gente desaconseja su uso, pero es una de las muchas herramientas que tenemos en este mundo 🙂

Primera aproximación

El caso más sencillo que podremos hacer es el siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Singleton
{
public:
  static Singleton *getInstance()
  {
    if (instance == NULL)
      instance = new Singleton();
    else
      std::cout << "Getting existing instance"<<std::endl;

    return instance;
  }

protected:
  Singleton()
  {
    std::cout << "Creating singleton" << std::endl;
  }

  virtual ~Singleton()
  {
  }

  Singleton(Singleton const&); 
  Singleton& operator=(Singleton const&);

private:
  static Singleton *instance;
};

Singleton* Singleton::instance=NULL;

De esta forma nos aseguramos de que el objeto sólo lo creamos una vez. Y por lo tanto, si durante la ejecución de nuestro programa queremos utilizar nuestro objeto de clase Singleton, tenemos que hacer:

1
Singleton *s = Singleton::getInstance();

En este caso, si la instancia no está creada, la creará (hay que observar que el constructor de la clase está protegido por lo tanto sólo se puede utilizar dentro de la clase Singleton y en clases derivadas), y si está creado utilizará la clase instanciada previamente (almacenada en el atributo instance). La magia está en que, tanto el atributo como el método getInstance() son estáticos, éstos dos serán globales para la clase y nos devolverán el objeto instanciado (que sólo podrá ser uno), y serán los métodos que creemos para nuestra clase los que se basen en cada objeto instanciado.

Por otro lado, el constructor de copia y el operador de asignación están protegidos para evitar que se puedan utilizar en nuestro programa y alguien pueda chafarnos este precioso funcionamiento, por ejemplo pudiendo crear dos instancias de esta clase (nuestra perdición).

En cuanto al destructor, nosotros elegimos, ¿queremos que se pueda destruir la clase? Podemos poner el destructor en la parte pública, así con un simple:

1
destroy s;

se podrá destruir la instancia de nuestro singleton (debemos tener cuidado y hacer que el método instance valga NULL de nuevo, o la próxima vez que utilicemos la instancia obtenida por getInstance() se puede liar).
Otra opción sería crear otro método estático:

1
2
3
4
5
6
7
8
void destroyInstance()
{
  if (instance != NULL)
  {
    destroy instance;
    instance = NULL;
  }
}

y mantener el destructor de nuestra clase protegido (o hacerlo privado). En estos método podemos poner más código, dependiendo del uso de nuestra clase. También podemos optar por la función atexit() de que llama a una serie de funciones (establecidas por nosotros) cuando el programa finaliza (siempre que termine bien, con return, o exit()).

¿Qué pasa cuando mi programa es multi-hilo?

Lo que hemos visto hasta ahora funciona bien cuando el programa tiene un sólo thread (o hilo), es decir, sólo tenemos una línea de órdenes en ejecución. Por un lado podemos pensar en procesadores de varios núcleos, es cierto que son capaces de ejecutar varias órdenes al mismo tiempo, pero nuestro programa puede aprovechar sólo un núcleo, por lo que normalmente los demás núcleos que no aprovechamos con una aplicación, el sistema operativo los destina a otros procesos, y no pasa nada.
El problema viene cuando, para aprovechar la tecnología de que disponemos, queremos aprovechar al máximo el procesador dentro de nuestra aplicación (imaginad un procesamiento matemático complejo) y queremos que nuestra aplicación pueda realizar varias cosas a la vez, es decir nuestra aplicación pueda estar ejecutando varias tareas simultáneamente. Aunque este concepto se asocia casi siempre con varios núcleos no siempre es así y a veces podemos ganar velocidad en un sistema mono-núcleo utilizando varios hilos (y también tendríamos el problema con los singletons) aprovechando esperas generadas, por ejemplo, por interacción con dispositivos.

Bueno, ¿ qué puede ocurrir ? Pues que varios hilos quieran simultáneamente solicitar una instancia de nuestro Singleton, y si es la primera vez, va a intentar crear una instancia, pero qué pasaría si un proceso entra en getInstance() y pasa el «if (instance == NULL)», entra en el constructor, y al mismo tiempo otro proceso entra también en getInstance() ? Pues que para cada proceso se creará una nueva instancia ya que no ha dado tiempo a asignar el valor del atributo instance, y eso puede pasar con varios procesos.

El primer problema es el multi-thread, C++ (versiones anteriores a C++11) no trae de forma nativa soporte para threads, por lo tanto, para los ejemplos utilizaré la biblioteca pthread que se podrá compilar en cualquier *nix, y también el ejemplo de C++11 para compiladores modernos, vamos a lanzar varios getInstance() en threads concurrentes, a ver qué pasa:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <iostream>
#include <pthread.h>
#include <cstdlib>
#include <unistd.h>

using namespace std;

class Singleton
{
public:
  static Singleton *getInstance()
  {
    if (instance == NULL)
      instance = new Singleton();
    else
      std::cout << "Getting existing instance"<<std::endl;

    return instance;
  }

protected:
  Singleton()
  {
    std::cout << "Creating singleton" << std::endl;
  }

  virtual ~Singleton()
  {
  }

  Singleton(Singleton const&); 
  Singleton& operator=(Singleton const&);

private:
  static Singleton *instance;
};

Singleton* Singleton::instance=NULL;

void *task (void*)
{
  Singleton *s = Singleton::getInstance();
  cout << "Thread con instancia"<<endl;
}

int main()
{
  for (unsigned i=0; i<100; ++i)
    {
      pthread_t thread;
      int rc = pthread_create(&thread, NULL, task, NULL);
    }

  pthread_exit(NULL);
  return 0;
}

Para compilar este ejemplo, hacemos gcc -o singlethread singlethread.cpp -lpthread

En ejemplo en C++11 es el siguiente (sí, podemos hacerlo mucho más sencillo aquí):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <iostream>
#include <thread>
#include <cstdlib>
#include <unistd.h>

using namespace std;

class Singleton
{
public:
  static Singleton *getInstance()
  {
    if (instance == nullptr)
      instance = new Singleton();
    else
      std::cout << "Getting existing instance"<<std::endl;

    return instance;
  }

protected:
  Singleton()
  {
    std::cout << "Creating singleton" << std::endl;
  }

  virtual ~Singleton()
  {
  }

  Singleton(Singleton const&); 
  Singleton& operator=(Singleton const&);

private:
  static Singleton *instance;
};

Singleton* Singleton::instance=nullptr;

int main()
{
  for (unsigned i=0; i<100; ++i)
    {
      thread([](){
      Singleton *s = Singleton::getInstance();
      cout << "Thread con instancia"<<endl;
    }).detach();
    }

  return 0;
}

Para compilar este con g++ : g++ -o singlethread11 singlethread11.cpp -std=c++11 -lpthread (C++ como lenguaje es más rápido e intuitivo, pero para este compilador, utilizamos la biblioteca de threads POSIX como antes).

En ambos casos en la salida podremos ver varios mensajes «Creating singleton» y hemos demostrado nuestro estrepitoso fallo en este sentido, nuestro Singleton se ha creado varias veces, y eso puede resultar desastroso para nuestro programa. En el caso de utilizar una estructura de este tipo para gestionar la configuración de nuestra aplicación, estaremos manteniendo varias veces en memoria la configuración, en principio no es demasiado malo, pero si modificamos la configuración para posteriormente guardarla, sólo se modificará una de las estructuras y al guardarse tendremos un problema.

Mañana, nos aproximamos al thread safety un poco más.

Modificado 22/04/2014: Gracias a Luis Cabellos por su sugerencia, he cambiado en el ejemplo de C++11 los NULL por nullptr.
Foto: Nan Palmero (Flickr CC-by)

También podría interesarte....