SDK Multiplataforma en C logo

SDK Multiplataforma en C

Hebras

❮ Anterior
Siguiente ❯

Las hebras o hilos son partes del mismo programa que pueden correr en paralelo.


Funciones

Thread*bthread_create (...)
intbthread_current_id (void)
voidbthread_close (...)
voidbthread_cancel (...)
uint32_tbthread_wait (...)
bool_tbthread_finish (...)
voidbthread_sleep (...)

Los hilos o hebras son diferentes caminos de ejecución dentro del mismo proceso (Figura 1). También son conocidos como procesos ligeros, ya que son más ágiles de crear y gestionar que los procesos propiamente dichos. Comparten código y espacio de memoria con el programa principal, por lo que es muy fácil intercambiar información entre ellos a través de variables de memoria. Una hebra comienza su ejecución en un método conocido como thread_main y, en el momento que se lanza, se ejecuta en paralelo con el hilo principal. Al igual que los procesos, son objetos controlados por el núcleo del sistema que dictaminará, en última instancia, si la hebras se ejecutarán en otro CPU-core (true multitasking) o lo compartirán (context switch).

  • Utiliza bthread_create para crear un nuevo hilo de ejecución.
  • Utiliza bthread_wait para obligar al hilo principal a que espere que se ejecute el hilo.
  • Esquema de un proceso con múltiples hilos de ejecución.
    Figura 1: Un proceso con múltiples hilos de ejecución.

1. Lanzando hebras

Cada llamada a bthread_create creará un nuevo hilo en paralelo comenzando en la función pasada como parámetro (thread_main). La forma "natural" de finalizarlo es retornando de thread_main, aunque es posible abortarlo desde el hilo principal.

Código básico para lanzar un hilo de ejecución paralelo.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
static uint32_t i_thread(ThData *data)
{
    // Do something
    ...
    // Thread execution ends
    return 0;
}

Thread *thread = bthread_create(i_thread, data, ThData);
// Main thread will continue here
// Second thread will run 'i_thread'

2. Variables compartidas

Cada nuevo hilo tiene su propio Segmento Stack por tanto, todas las variables automáticas, llamadas a función y reservas dinámicas serán privadas a dicho hilo. Pero también puede recibir datos globales del proceso a través del parámetro data de thread_main. Debemos tener cuidado al acceder a datos globales a través de múltiples hilos concurrentes, ya que modificaciones realizadas por otros hilos pueden alterar la ejecución lógica del código produciendo errores muy difíciles de depurar. El programa (Listado 1) es correcto para programas de un solo hilo, pero si la variable vector es accedida por dos hebras simultáneas, puede derivar en un error Segmentatin Fault si la hebra-1 libera la memoria mientras la hebra-2 está ejecutando el bucle.

Listado 1: Acceso peligroso a variables compartidas.
1
2
3
4
5
6
7
8
if (shared->vector != NULL)
{
    shared->total = 0;
    for(i = 0; i < shared->n; i++)
        shared->total += shared->vector[i];
    bmem_free(shared->vector);
    shared->vector = NULL;
}

Para evitar este problema, deberemos proteger los accesos a variables compartidas a través de un Mutex (Listado 2). Este mecanismo de Exclusión mutua garantiza que solo un hilo puede acceder al recurso en un instante de tiempo. Un hilo será detenido si pretende ejecutar el código situado entre bmutex_lock y bmutex_unlock si otro hilo se encuentra dentro de esta sección crítica.

Listado 2: Acceso seguro a variables compartidas.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
bmutex_lock(shared->mutex);
if (shared->vector != NULL)
{
    shared->total = 0;
    for(i = 0; i < shared->n; i++)
        shared->total += shared->vector[i];
    bmem_free(shared->vector);
    shared->vector = NULL;
}
bmutex_unlock(shared->mutex);

3. Ejemplo multi-hilo

Lo complicado de la programación multi-hilo es descomponer una solución en partes que puedan correr en paralelo y organizar las estructuras de datos para que esto se pueda llevar a cabo de la forma más equilibrada posible. En (Listado 3) el programa correrá cuatro veces más rápido (x4) ya que se ha hecho una división perfecta del problema (Figura 2). Esto no es más que un ejemplo teórico y este resultado será muy difícil de conseguir en situaciones reales. También deberemos reducir al mínimo la cantidad de variables compartidas y el tiempo de las secciones críticas, de lo contrario los posibles inter-bloqueos reducirán la ganancia.

Esquema de varias hebras colaborando en el cálculo de un vector.
Figura 2: Colaboración de cuatro hebras en el cálculo de un vector.
Listado 3: Procesamiento multi-hilo de un vector muy grande.
 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
typedef struct _app_t App;
typedef struct _thdata_t ThData;

struct _app_t
{
    uint32_t total;
    uint32_t n;
    uint32_t *elems;
    Mutex *mutex;
};

struct _thdata_t
{
    uint32_t thread_id;
    uint32_t start;
    uint32_t end;
    uint64_t time;
    App *app;
};

static uint32_t i_thead(ThData *data)
{
    uint32_t i, total = 0;
    uint64_t t1 = btime_now();
    for (i = data->start; i < data->end; ++i)
    {
        // Simulates processing
        uint32_t time = bmath_randi(0, 100);
        bthread_sleep(time);
        total += data->app->elems[i];
    }

    // Mutual exclusion access to shared variable 'total'
    bmutex_lock(data->app->mutex);
    data->app->total += total;
    bmutex_unlock(data->app->mutex);
    data->time = (btime_now() - t1) / 1000;
    return data->thread_id;
}

// Threads creating function
uint32_t i, m;
uint64_t t;
App app;
ThData thdata[4];
Thread *thread[4];

// App data vector
i_init_data(&app);
app.mutex = bmutex_create();
m = app.n / 4;

// Thread data
for (i = 0; i < 4; ++i)
{
    thdata[i].thread_id = i;
    thdata[i].app = &app;
    thdata[i].start = i * m;
    thdata[i].end = (i + 1) * m;
}

// Launching threads
t = btime_now();
for (i = 0; i < 4; ++i)
    thread[i] = bthread_create(i_thead, &thdata[i], ThData);

// Wait for threads end
for (i = 0; i < 4; ++i)
{
    uint32_t thid = bthread_wait(thread[i]);
    bstd_printf("Thread %d finished in %d ms.\n", thid, thdata[thid].time);
    bthread_close(&thread[i]);
}

// Process total time
t = (btime_now() - t) / 1000;
bstd_printf("Proccessing result = %d in %d ms.\n", app.total, t);

bmutex_close(&app.mutex);
Listado 4: Resultado.
1
2
3
4
5
Thread 0 finished in 13339 ms.
Thread 1 finished in 12506 ms.
Thread 2 finished in 12521 ms.
Thread 3 finished in 12999 ms.
Proccessing result = 499500 in 13344 ms.

bthread_create ()

Crea un nuevo hilo de ejecución, que arranca en thmain.

Thread*
bthread_create(FPtr_thread_main thmain,
               type *data,
               type);
thmain

La función de inicio de la hebra thread_main. Se pueden pasar datos compartidos mediante el puntero data.

data

Datos pasados como parámetro a thmain.

type

Tipo de data.

Retorna

Manejador de la hebra. Si la función falla, retorna NULL.

Observaciones

El hilo se ejecutará en paralelo hasta que thmain retorne o se llame a bthread_cancel. Lanzando hebras.


bthread_current_id ()

Retorna el identificador manejador de la hebra actual, es decir, la que está corriendo cuando se llama a esta función.

int
bthread_current_id(void);

Retorna

Manejador de la hebra.


bthread_close ()

Cierra el manejador de la hebra y libera recursos.

void
bthread_close(Thread **thread);
thread

Manejador de la hebra. Será puesto a NULL tras el cierre.

Observaciones

Si el hilo todavía se está ejecutando, esta función no lo finaliza. Como cualquier otro objeto, un hilo siempre debe cerrarse, incluso si ya ha terminado su ejecución. Lanzando hebras.


bthread_cancel ()

Fuerza la terminación del hilo especificado.

void
bthread_cancel(Thread *thread);
thread

Manejador de la hebra.

Observaciones

No es recomendable llamar a esta función. No se realizará una salida "limpia" del hilo. Si se encuentra dentro de una sección crítica, esta no será liberada. Tampoco de liberará la memoria dinámica reservada de forma privada por el hilo. La forma correcta de finalizar un hilo de ejecución es retornando de thmain. Pueden utilizarse variables compartidas (Exclusión mutua) para indicarle a un hilo que debe terminar de forma limpia.


bthread_wait ()

Detiene al hilo que llama a esta función hasta que thread termina su ejecución.

uint32_t
bthread_wait(Thread *thread);
thread

Manejador de la hebra a la que debemos esperar.

Retorna

El valor de retorno del hilo. Si ocurre algún error, retorna UINT32_MAX.


bthread_finish ()

Comprueba si la hebra sigue en ejecución.

bool_t
bthread_finish(Thread *thread,
               uint32_t *code);
thread

Manejador de la hebra.

code

El valor de retorno de la función thmain (si ha terminado). Puede ser NULL.

Retorna

TRUE si el hilo ha terminado, FALSE si no.

Observaciones

Esta función retorna inmediatamente.


bthread_sleep ()

Suspende la ejecución de la hebra actual (la que llama a esta función) durante un número determinado de milisegundos.

void
bthread_sleep(const uint32_t milliseconds);
milliseconds

Intervalo de tiempo (en milisegundos) que durará la suspensión.

Observaciones

Realiza una suspensión "pasiva", donde ningún "bucle vacío" será ejecutado. El hilo es decartado por el scheduler y reactivado posteriormente.

❮ Anterior
Siguiente ❯