UC3M

Grado en Ing. Telemática/Sist. Audiovisuales/Sist. de Comunicaciones

Arquitectura de Sistemas

Septiembre 2017 - Enero 2018

11.5. Actividades

11.5.1. Procesado concurrente (hilos) de una tabla

Plan de trabajo

El presente ejercicio tiene por objetivo analizar un programa que realiza el procesado concurrente de una tabla. Esta versión hace un uso sencillo de un patrón de programación concurrente que reparte la carga entre los diferentes hilos de una aplicación para realizar el procesado parcial de cada una de las partes. En este caso, el uso de una infraestructura multi-núcleo, disponible en muchas máquinas actuales, ayudará a reducir los tiempos de ejecución.

La idea básica es procesar una estructura de datos larga y repartir el trabajo en varias hebras:

  1. En el ejemplo dado hay 5 hebras, cada una de las cuales suma un quinto de la tabla, y guarda la solución en una tabla intermedia.

  2. Cuando todas las hebras han terminado su ejecución y salen de la función do_work, la función del main suma las contribuciones parciales de cada una de las hebras. Para esperar por el hilo del main utilizará un pthread_join por cada uno de los hilos.

  3. Fíjese que el código no presenta anomalias de ejecución pues antes de ejecutarse cada hebra tiene un bloque de memoria en exclusiva (Puede probarlo con la herramienta helgrind).

  4. También es cierto que la mezcla de los datos de las hebras es segura pues la función de main encargada de sumar no tiene lugar mientras la hebra está en ejecución.

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

#define NTHREADS      5
#define ARRAYSIZE   100000000
#define ITERATIONS   ARRAYSIZE / NTHREADS

double sum=0.0;
double a[ARRAYSIZE];
double mysums[NTHREADS];

void *do_work(void *tid) 
{
  int i, start, *mytid, end;
  double mysum=0.0;

  /* Initialize my part of the global array and keep local sum */
  mytid = (int *) tid;
  start = (*mytid * ITERATIONS);
  end = start + ITERATIONS;
  printf ("\n[Thread %5d] Doing iterations \t%10d to \t %10d",*mytid,start,end-1); 
  for (i=start; i < end ; i++) 
    {
      a[i] = i * 1.0;
      mysum = mysum + a[i];
    }
  mysums[*mytid]=mysum;
  printf ("\n[Thread %5d] Sum %e",*mytid,mysum); 
  pthread_exit(NULL);
}


int main(int argc, char *argv[])
{
  int i, start, tids[NTHREADS];
  pthread_t threads[NTHREADS];
  pthread_attr_t attr; 
  pthread_attr_init(&attr);
  pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
  for (i=0; i<NTHREADS; i++) {
    tids[i] = i;
    pthread_create(&threads[i], &attr, do_work, (void *) &tids[i]);
    }

  /* Wait for all threads to complete then print global sum */ 
  for (i=0; i<NTHREADS; i++) {
    pthread_join(threads[i], NULL);
  }  
  /* Computing the output*/ 
  sum=0.0;
  for (i=0;i<NTHREADS;i++)
  { 
    sum = sum + mysums[i];     
  }
  printf("\n[MAIN] Total Sum= %e\n",sum);
  /* Clean up and exit */
  pthread_attr_destroy(&attr);  
  pthread_exit (NULL);
}
 

Ahora es el momento de experimentar los beneficios en temas de rendimiento que ofrece el multi núcleo. Para ello se le pide que realice los siguientes cambios sobre el código que se le da:

  1. Examine el código, compílelo con el gcc y ejecútelo, midiendo el tiempo que tarda en ejecutarse. Para ello puede usar el comando time de Linux. Por ejemplo, si su ejecutable se llama concurrent_loop, Puede usar time ./concurrent_loop para verificar el tiempo total de ejecución de su programa.

  2. Lo modifique de tal manera que sólo haya un hilo en la aplicación. Calcule de nuevo cuánto tarda en ejecutarse su aplicación. ¿Por qué sale más alto que en el caso anterior?

  3. Cambie el código para que no haya variables globales y todas sean pasadas al hilo a través de su puntero de inicialización. Todos las tablas deberían de estar creadas en la función main. Esos datos pueden ir empaquetados en una estructura.

  4. Por último, vuelva a modificar el código para que en vez de utilizar concurrencia, se utilice la programación más tradicional (sin hebras). Para ello, una forma sencilla de conseguir esa meta es la de eliminar las referencias a los hilos y llamar a la función directamente desde el bloque de la función main. Mida el tiempo que tarda en ejecutarse esta nueva versión de su código. ¿Sale en medio de los dos tiempos anteriores? ¿A qué cree que es debido este coste?