Inicio > algoritmos, C/C++, Cosas que damos por hechas, curioso, General > Singletons en C++ y alguna nota sobre thread safety (I)

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:

  • Una conexión de base de datos para nuestra aplicación. Bueno, a veces necesitamos varias, pero muchas veces sólo necesitamos una, y queremos referirnos a ella en cualquier punto de nuestro programa (y no queremos una variable global ni nada de eso).
  • Un objeto de configuración general del programa. Imaginemos que en el constructor leemos el archivo de configuración y generamos un árbol con las opciones de nuestra aplicación. Dicho árbol no debe ser generado más veces, sólo cuando arrancamos el programa.
  • Un logger, que configuraremos una sola vez y nos referiremos al objeto cuando queramos escribir algo.
  • Almacén de caché. Por ejemplo para almacenar valores traídos de base de datos que vamos a utilizar en el transcurso de nuestro programa, así evitamos traerlos más veces.
  • Acceso a un recurso exclusivo, por poner un ejemplo un hardware especializado.
  • Muchos más usos.

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)

  1. Sebastián
    21 abril, 2014 16:44 | #1
    Usando Mozilla Firefox Mozilla Firefox 28.0 en Windows Windows XP

    Que tal, sólo quería informarte que no puedo visualizar los bloques de código. Sólo el primer bloque, en el cual se define la clase Singleton, se visualiza, en los demás indica lo siguiente:
    “GeSHi Error: GeSHi could not find the language ccp (using path /home/gaspy/www/totaki.com/www/poesiabinaria/wp-content/plugins/codecolorer/lib/geshi/) (code 2)”. Sólo lo he probado en Firefox y chrome. Espero se pueda solucionar, dado que me interesa mucho el artículos.
    Saludos y gracias.

  2. Gaspar Fernández
    21 abril, 2014 16:59 | #2
    Usando Mozilla Firefox Mozilla Firefox 28.0 en Ubuntu Linux Ubuntu Linux

    @Sebastián
    Sebastian !! Muchas gracias por tu comentario, se me pasó, al plugin que uso para los bloques de código le dije ccp en lugar de cpp y se lió todo. Aquí tienes el código, y mañana se publica también la segunda parte del artículo.

  3. 21 abril, 2014 22:59 | #3
    Usando Mozilla Firefox Mozilla Firefox 28.0 en Ubuntu Linux Ubuntu Linux

    NULL en C++11 se escribe nullptr y el uso de auto mejora aun más el código, e.g:

    auto s = Singleton::getInstance();

    Por lo demas, espero la siguiente parte del articulo :)

    • Gaspar Fernández
      22 abril, 2014 10:38 | #4
      Usando Mozilla Firefox Mozilla Firefox 28.0 en Ubuntu Linux Ubuntu Linux

      Gracias Luis. Cambio el NULL en el ejemplo. nullptr está muy bien y ¡no me acostumbro a usarlo! no puede ser.
      Por lo del auto, yo lo veo más por pereza y me encanta, mi uso preferido está en los iteradores, cuando tenemos un map >::iterator lo cambias por un auto, y te da hasta alegría :) pero es sólo un Singleton* (más del doble de letras que auto, pero bueno)

  1. abril 21st, 2014 at 18:26 | #1
  2. abril 22nd, 2014 at 10:59 | #2

Current ye@r *

Top