Publi

Pinceladas de concurrencia y Hello world usando pthreads


En la actualidad, es muy común ver CPUs es dual-core o quad-core (por lo menos), aunque también sigue habiendo sistemas con un sólo núcleo.

Antes de nada, quiero decir que en este post sólo quiero dar algunas pinceladas, y un poco de código como introducción a este “mundo”, por lo que las explicaciones serán algo rápidas, me refiero a que estaré simplificando mucho, en el fondo, todo es un poco más complicado… pero al menos daré datos suficientes para poder profundizar más en el tema.

En sistemas de varias CPU o varios núcleos, cuando le damos un uso normal, el sistema operativo, va repartiendo las diferentes tareas que tiene que hacer en los diferentes núcleos de nuestro sistema, el procesador es capaz de ejecutar varias cosas al mismo tiempo, ya que un núcleo sólo puede ejecutar una tarea, varios núcleos podrán ejecutar más tareas.
Ahora podríamos pensar: “llevamos disfrutando de la multitarea muchos años, y mi CPU ha sido de un core toda la vida, ¿cómo es posible?” – Eso ha sido realidad gracias a que la CPU ha sido capaz de repartir en el tiempo las tareas muy rápidamente, y nosotros, casi no podemos darnos cuenta de lo rápido que se cambia de tareas. Y todavía tiene sentido, ya que normalmente tendremos en ejecución decenas o cientos de tareas a la vez.

Ahora bien, ¿ tiene sentido crear una aplicación crear una aplicación que ejecute varias tareas al mismo tiempo, si mi CPU sólo puede ejecutar una tarea ?
Voy a dar algunos casos simples:

  • Un programa con GUI que está realizando una tarea muy pesada. Mientras se ejecuta la tarea pesada, el usuario pierde interacción con la aplicación, por lo que ésta puede lanzarse en otro hilo de ejecución, por ejemplo para atender si el usuario pulsa un botón para cancelar.
  • Un servidor que debe atender a varios clientes simultáneamente.
  • Un programa que deba hacer una computación intensa, un cálculo, generar información…

Aunque, en el último caso, si tiene que hacer una computación intensa, a lo mejor el sistema operativo tarda más tiempo dando paso a las tareas que lo que nosotros vamos a ganar repartiendo la tarea en dos… esto sería cierto si nuestras tareas fueran sólo computación con lecturas y escrituras en memoria pero, si tenemos que realizar acceso a disco o a otros dispositivos, esto cambia mucho.
El tiempo de acceso y diálogo con los dispositivos es mucho (muchísimo) más largo que el tiempo que podemos invertir en cualquier instrucción de CPU, por lo tanto, mientras nuestra aplicación está leyendo o escribiendo en disco, probablemente esté esperando que esto sea efectivo y tarde bastante tiempo en serlo, por lo que otro proceso puede tomar el control de la CPU para hacer algo útil con ella. En este caso sí que sería útil que una aplicación que realice algo pesado pueda dividirse en varios hilos de ejecución, así mientras uno lee o escribe datos del disco, el otro hilo puede estar procesando, y la tarea tardará menos tiempo en realizarse.

Ahora bien, para realizar tareas de forma concurrente podemos hacerlo de dos formas, creando varios procesos, en los que cada uno procesará un trozo o creando hilos. La diferencia es que los procesos deben tener su parte de código, datos y pila entre otra información que debe gestionar el sistema operativo. Un hilo de ejecución o thread, sólo tendrá una zona de pila propia, así como información sobre el estado de ejecución de la tarea que está realizando, por lo que la zona de código y datos es compartida entre todos los hilos de un proceso; podemos decir que cuando se inicia un proceso, también se inicia un hilo de ejecución de ese proceso.

Eso sí, con los procesadores cada vez más potentes, capaces de ejecutar varias tareas al mismo tiempo, ahora de verdad, tiene mucho más sentido dividir nuestras tareas pesadas en hilos, de esta forma aprovecharemos toda la potencia de nuestro procesador para nuestra tarea.

Creo que va siendo hora de introducir algo de código, lo haremos utilizando pthreads o hilos de ejecución siguiendo el estándar POSIX:

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
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

void *newtask(void *null)
{
   printf("Hello world! I'm a new thread\n");
   sleep(1);
   printf("Goodbye world! I'm a thread about to die\n");
   pthread_exit(NULL);
}

int main (int argc, char *argv[])
{
   pthread_t thread;
   int rc;

   printf ("Main process just started.\n");
   rc = pthread_create(&thread, NULL, newtask, NULL);
   if (rc)
     {
       printf("ERROR in pthread_create(): %d\n", rc);
       exit(-1);
     }

   printf ("Main process about to finish.\n");
   /* Last thing that main() should do */
   pthread_exit(NULL);
}

Este ejemplo lo compilamos incluyendo pthread, de la siguiente manera:

$ gcc -o onethread onethread.c -lpthread

Casi no hacemos nada, sólo ejecutamos un programa que hará que otro thread ejecute una tarea determinada, esta tarea es escribir un par de mensajes y esperar un tiempo entre ellos. Podemos ver que su salida es algo así:

Main process just started.
Main process about to finish.
Hello world! I’m a new thread
[ wait for a second ]
Goodbye world! I’m a thread about to die

Los dos primeros mensajes corresponden al thread principal, uno cuando se inicia y otro cuando se finaliza, aunque antes de finalizar invocamos al otro thread que escribirá los otros dos mensajes. Como vemos, cada thread se ha ejecutado de forma independiente… aunque, vamos a verlo un poco más claro con el siguiente código:

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
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

void *newtask(void *null)
{
   printf("Hello world! I'm a new thread\n");
   int i;

   for (i=0; i<10;++i)
     {
       printf (" THREAD: %d\n", i);
       usleep(60);
     }
   printf("Goodbye world! I'm a thread about to die\n");
   pthread_exit(NULL);
}

int main (int argc, char *argv[])
{
   pthread_t thread;
   int rc;
   int i;

   printf ("Main process just started.\n");
   rc = pthread_create(&thread, NULL, newtask, NULL);
   if (rc)
     {
       printf("ERROR in pthread_create(): %d\n", rc);
       exit(-1);
     }

   for (i=0; i<10;++i)
     {
       usleep(100);
       printf (" MAIN: %d\n", i);
     }
   printf ("Main process about to finish.\n");
   /* Last thing that main() should do */
   pthread_exit(NULL);
}

Ahora, hemos hecho que tanto el thread principal (MAIN) como el secundario (THREAD) escriban en pantalla varios mensajes dentro de un bucle for, van a estar contando del 0 al 9, aunque esperando un pequeño tiempo entre escritura y escritura (en lugar de hacer tareas más o menos pesadas que tarden un tiempo, lo simulamos con esperas).

Veremos en la ejecución que se irán escribiendo alternativamente los mensajes tanto de MAIN como de thread, de forma entrelazada, ahora sí que podemos decir que se están ejecutando de forma concurrente.

Esta es una prueba de ejecución en mi equipo:

Main process just started.
Hello world! I’m a new thread
THREAD: 0
THREAD: 1
MAIN: 0
THREAD: 2
MAIN: 1
THREAD: 3
THREAD: 4
MAIN: 2
THREAD: 5
MAIN: 3
THREAD: 6
MAIN: 4
THREAD: 7
THREAD: 8
MAIN: 5
THREAD: 9
MAIN: 6
Goodbye world! I’m a thread about to die
MAIN: 7
MAIN: 8
MAIN: 9
Main process about to finish.

Con esto podemos realizar programas multithread sencillos, podemos hacer, por ejemplo, que aunque tengamos un hilo de ejecución bloqueado, otro thread pueda seguir aceptando comunicación de usuario, aunque todavía tenemos algunas cosas más por ver.

Foto: Toastyken (Flickr) CC-by

También podría interesarte...

Leave a Reply