Inicio > algoritmos, C/C++, Concurrencia > Concurrencia, cuando varios hilos (threads) pelean por el acceso a un recurso [ejemplos en C]

Concurrencia, cuando varios hilos (threads) pelean por el acceso a un recurso [ejemplos en C]

Si estamos desarrollando nuestra aplicación multi-hilo y además compartimos información entre el hilo principal y el secundario, o entre varios hilos tenemos que tener en cuenta el tipo de acceso a las variables compartidas.
Por ejemplo, si sólo vamos a permitir que un hilo escriba, y todos los demás lean, casi no vamos a tener problemas, pero si cualquier thread de nuestra aplicación va a poder escribir en cualquier momento, tenemos que tener cuidado con cómo lo hacemos, ya que si varios hilos intentan escribir un recurso, sólo la última escritura será efectiva, y si el valor antiguo de la variable es crítico, en muchos casos no se tendrá en cuenta.

Por ejemplo, tenemos una aplicación que gestiona nuestra colección de películas, y en un momento del tiempo tenemos 50 películas, pero otro thread se encarga de sincronizar la base de datos de películas con un servidor de Internet, si mientras se realiza la sincronización insertamos 3 películas (y no al final, de la lista), tal vez el thread que intenta sincronizar vea que son 50 películas, y el listado de 50 películas no coincida con las que había antes, sino que estén las 3 nuevas entre medias, por lo que en el servidor habría películas eliminadas y tendremos un problema con la información enviada.

En este caso, lo que debemos hacer es proteger el acceso a la sección crítica (nuestro listado de películas), para que, mientras estemos introduciendo películas no existan sincronizaciones y mientras hay sincronizaciones no se pueda modificar la información. Para ello, haremos uso de la exclusión mutua, o mutex.

Para intentar hacer un ejemplo un poco visible, vamos a incrementar números, aunque entre la lectura del número y la escritura de nuevo del valor, vamos a introducir una tarea de cálculo que tardará un tiempo variable en completarse, así algunos threads podrán hacerlo antes que otros. El resultado deseado es que el número se vaya incrementando a cada operación, pero el resultado real difiere un poco:

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>

struct thread_vars_t
{
  int number;
};

int numberExists(int arr[], int current)
{
  int i;

  for (i=0; i<current; ++i)
    {
      if (arr[current]==arr[i])
    return 1;
    }

  return 0;
}

void numberSearch()
{
  /* A time taking task */
  int numbers[100];
  int i;

  for (i=0; i<100; ++i)
    {
      numbers[i] = rand()%101;
      while (numberExists(numbers, i))
    numbers[i] = rand()%101;
    }
}


void *newtask(void *_number)
{
  struct thread_vars_t *vars = _number;

  int number = vars->number;
  numberSearch();
  vars->number = number+1;

  printf ("THREAD: number = %d\n", vars->number);

  pthread_exit(NULL);
}

int main (int argc, char *argv[])
{
   pthread_t thread;
   int rc;
   int i;
   struct thread_vars_t *vars = malloc (sizeof(struct thread_vars_t));

   vars->number = 0;

   printf ("Main process just started.\n");
   for (i=0; i<10; ++i)
     {
       rc = pthread_create(&thread, NULL, newtask, vars);
       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);
}

El resultado es más o menos este:

$ ./sharedvar
Main process just started.
THREAD : number = 1
THREAD : number = 1
THREAD : number = 2
THREAD : number = 2
THREAD : number = 3
Main process about to finish.
THREAD : number = 4
THREAD : number = 4
THREAD : number = 3
THREAD : number = 4
THREAD : number = 4

¿Qué ha pasado? Algunos threads cuando leyeron el valor de la variable valía 0 (y se dio en dos ocasiones), por tanto lo incrementaron a 1, otros, al entrar valía 1 (otros dos casos), y lo incrementaron a 2, en otros casos, valía 2 y lo incrementaron a 3…
Es decir, varios procesos leyeron el mismo valor, y a la hora de escribir el nuevo valor no se ha tenido en cuenta el cambio. Esto es lo que se llama condición de carrera.

¿Cómo podemos arreglar esto? La solución es poniendo estructuras que se den cuenta de que un recurso está siendo utilizado en un momento, por ejemplo, darnos cuenta de que cuando leemos el valor de la variable nadie está calculando ningún valor, así, si estamos haciendo un cálculo, ser los únicos que estamos ahí.
¿Perdemos rendimiento? Sí, un poco, porque estamos provocando que mientras un hilo está en ejecución, nadie esté en la sección crítica, pero es deseable para evitar resultados inesperados como el del ejemplo. Es más, muchas veces los threads realizarán tareas que sí se pueden simultanear y en esos casos estaremos aprovechando bien los recursos. Sólo será en la sección crítica (cuando estemos trabajando con el número) cuando bloquearemos otros hilos con el mutex.

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>

struct thread_vars_t
{
  int number;
  pthread_mutex_t mutex;
};

int numberExists(int arr[], int current)
{
  int i;

  for (i=0; i<current; ++i)
    {
      if (arr[current]==arr[i])
    return 1;
    }

  return 0;
}

void numberSearch()
{
  /* A time taking task */
  int numbers[100];
  int i;

  for (i=0; i<100; ++i)
    {
      numbers[i] = rand()%101;
      while (numberExists(numbers, i))
    numbers[i] = rand()%101;
    }
}


void *newtask(void *_number)
{
  struct thread_vars_t *vars = _number;

  /* BLOCK */
  pthread_mutex_lock(&vars->mutex);
  /* BLOCK */

  int number = vars->number;
  numberSearch();
  vars->number = number+1;

  /* UNBLOCK */
  pthread_mutex_unlock(&vars->mutex);
  /* UNBLOCK */

  printf ("THREAD: number = %d\n", vars->number);

  pthread_exit(NULL);
}

int main (int argc, char *argv[])
{
   pthread_t thread;
   int rc;
   int i;
   struct thread_vars_t *vars = malloc (sizeof(struct thread_vars_t));

   pthread_mutex_init(&vars->mutex, NULL);
   vars->number = 0;

   printf ("Main process just started.\n");
   for (i=0; i<10; ++i)
     {
       rc = pthread_create(&thread, NULL, newtask, vars);
       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);
}

En este caso, la ejecución resultará así:

$ ./simplemutex
Main process just started.
THREAD: number = 1
Main process about to finish.
THREAD: number = 2
THREAD: number = 3
THREAD: number = 4
THREAD: number = 5
THREAD: number = 6
THREAD: number = 7
THREAD: number = 8
THREAD: number = 9
THREAD: number = 10

Foto: Daryl L. Hunter (Flickr) CC-by

  1. 9 febrero, 2014 2:17 | #1
    Usando Google Chrome Google Chrome 34.0.1825.4 en Linux Linux

    Con este blog, siempre se aprende algo nuevo.
    Felicidades por tan excelente trabajo Gaspar!

  2. Gaspar Fernández
    14 marzo, 2014 16:38 | #2
    Usando Mozilla Firefox Mozilla Firefox 27.0 en Ubuntu Linux Ubuntu Linux

    @sophiekovalevsky
    Muchas gracias! :)

  1. agosto 5th, 2014 at 10:52 | #1

Current ye@r *

Top