Publi

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)

También podría interesarte....

There are 16 comments left Ir a comentario

  1. Sebastián /
    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 / Post Author
    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. Pingback: BlogESfera.com /

  4. Luis Cabellos /
    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 🙂

    1. Gaspar Fernández / Post Author
      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)

  5. Pingback: Singleton en C++ thread safe y recomendaciones /

  6. pikachu /
    Usando Google Chrome Google Chrome 81.0.4044.138 en Windows Windows NT

    You’ve written nice post, I am gonna bookmark this page, thanks for info. I actually appreciate your own position and I will be sure to come back here. Thanks. wheelie cross

  7. Alicia Calculadora /
    Usando Google Chrome Google Chrome 116.0.0.0 en Windows Windows NT

    Las publicaciones de su blog son muy informativas, especialmente para los informáticos. Publicaciones imprescindibles para desarrolladores web.

  8. Rizk /
    Usando Google Chrome Google Chrome 107.0.5621.0 en Windows Windows NT

    The Capcut Pro
    is a premium video editing app that allows you to customize how long images or videos appear in the video, as well as add cool effects, text to speech and more.

  9. Nick /
    Usando Google Chrome Google Chrome 116.0.0.0 en Windows Windows NT

    That’s amazing to know and btw, this site is pretty interesting Residential Dumpster Rental

  10. MOD /
    Usando Google Chrome Google Chrome 116.0.0.0 en Windows Windows NT

    For those who prefer visual learning, the website offers video tutorials that guide you through the process of installing and using mod apks effectively.

  11. Jack Show /
    Usando Google Chrome Google Chrome 117.0.0.0 en Windows Windows NT

    However, the term «perfect» can be subjective, as it depends on individual preferences and the specific needs of the user. Different video editing apps may be more suitable for different purposes, and what one person considers perfect, another may not.
    CapCut PC

  12. Jack Show /
    Usando Google Chrome Google Chrome 117.0.0.0 en Windows Windows NT

    capcup mod apk

  13. Jack Show /
    Usando Google Chrome Google Chrome 117.0.0.0 en Windows Windows NT

    CapCut PC
    Capcut PC is a fully-compatible application to edit content on Windows for free. Everyone maintains their social media accounts to be more social and interactive among social media influencers and content creators. TikTok is another social application or platform where people post content on different trending discussions.

  14. Rohan Rajput /
    Usando Google Chrome Google Chrome 118.0.0.0 en Windows Windows NT

    Are you looking to Explore Real Delhi Escorts ? You have landed on our website Nisha Sharma Escort Service in Delhi, which will now take you to the world of Escort and will match you with beautiful girls in Delhi. So how are you guys? we have been providing Escort Services For the last eight years.

  15. Poonam /
    Usando Google Chrome Google Chrome 120.0.0.0 en Windows Windows NT

    i just read the post and the comments above it looks amazing thanks for sharing such information with us. Regards: Leather Shoes

Leave a Reply