Pages

Friday, April 28, 2017

A Thread Programming Example

Welcome » NERWous C » Examples
  1. Current Literature
  2. NERWous C Sample


Current Literature
Thread programming is one of the most popular way to write concurrent programs within a computer. It does not depend on a concurrent programming language. Instead it uses a serial programming language (such as C or C++), and invokes concurrency-enabling functions from a thread library. The library interacts with the operating system to time-share a single CPU into slices of executions or threads. A multi-core computer can distribute the threads into the CPU cores, thus boosting the concurrency and resulting in potentially higher performance.

This example comes from the School of Computer Science and Informatics of Cardiff University. It exercises the thr_create() library call to create multiple threads and thr_join() call to join them back at a later time. The source code of the example is copied verbatim below. The source page has an explanation of each thread.
#define _REENTRANT
#include <stdio.h>
#include <thread.h>

/* Function prototypes for thread routines */
void *sub_a(void *);
void *sub_b(void *);
void *sub_c(void *);
void *sub_d(void *);
void *sub_e(void *);
void *sub_f(void *);

thread_t thr_a, thr_b, thr_c;

void main()
{
    thread_t main_thr;

    main_thr = thr_self();
    printf("Main thread = %d\n", main_thr); 

    if (thr_create(NULL, 0, sub_b, NULL, THR_SUSPENDED|THR_NEW_LWP, &thr_b))
        fprintf(stderr,"Can't create thr_b\n"), exit(1);

    if (thr_create(NULL, 0, sub_a, (void *)thr_b, THR_NEW_LWP, &thr_a))
        fprintf(stderr,"Can't create thr_a\n"), exit(1);

    if (thr_create(NULL, 0, sub_c, (void *)main_thr, THR_NEW_LWP, &thr_c))
        fprintf(stderr,"Can't create thr_c\n"), exit(1);

    printf("Main Created threads A:%d B:%d C:%d\n", thr_a, thr_b, thr_c); 
    printf("Main Thread exiting...\n"); 
    thr_exit((void *)main_thr);
}

void *sub_a(void *arg)
{
    thread_t thr_b = (thread_t) arg;
    thread_t thr_d;
    int i;

    printf("A: In thread A...\n"); 

    if (thr_create(NULL, 0, sub_d, (void *)thr_b, THR_NEW_LWP, &thr_d))
        fprintf(stderr, "Can't create thr_d\n"), exit(1);

    printf("A: Created thread D:%d\n", thr_d); 

    /* process 
    */
    for (i=0;i<1000000*(int)thr_self();i++);
    printf("A: Thread exiting...\n"); 
    thr_exit((void *)77);
}

void * sub_b(void *arg)
{
    int i;

    printf("B: In thread B...\n"); 

    /* process 
    */

    for (i=0;i<1000000*(int)thr_self();i++);
    printf("B: Thread exiting...\n"); 
    thr_exit((void *)66);
}

void * sub_c(void *arg)
{
    void *status;
    int i;
    thread_t main_thr, ret_thr;

    main_thr = (thread_t)arg;

    printf("C: In thread C...\n"); 

    if (thr_create(NULL, 0, sub_f, (void *)0, THR_BOUND|THR_DAEMON, NULL))
        fprintf(stderr, "Can't create thr_f\n"), exit(1);

    printf("C: Join main thread\n"); 

    if (thr_join(main_thr,(thread_t *)&ret_thr, &status)) 
        fprintf(stderr, "thr_join Error\n"), exit(1);

    printf("C: Main thread (%d) returned thread (%d) w/status %d\n", main_thr, ret_thr, (int) status); 

    /* process 
    */

    for (i=0;i<1000000*(int)thr_self();i++);
    printf("C: Thread exiting...\n"); 
    thr_exit((void *)88);
}

void * sub_d(void *arg)
{
    thread_t thr_b = (thread_t) arg;
    int i;
    thread_t thr_e, ret_thr;
    void *status;

    printf("D: In thread D...\n"); 

    if (thr_create(NULL, 0, sub_e, NULL, THR_NEW_LWP, &thr_e))
        fprintf(stderr,"Can't create thr_e\n"), exit(1);

    printf("D: Created thread E:%d\n", thr_e); 
    printf("D: Continue B thread = %d\n", thr_b); 

    thr_continue(thr_b);
    printf("D: Join E thread\n"); 

    if(thr_join(thr_e,(thread_t *)&ret_thr, &status)) 
        fprintf(stderr,"thr_join Error\n"), exit(1);

    printf("D: E thread (%d) returned thread (%d) w/status %d\n", thr_e, 
    ret_thr, (int) status); 

    /* process 
    */

    for (i=0;i<1000000*(int)thr_self();i++);
    printf("D: Thread exiting...\n"); 
    thr_exit((void *)55);
}

void * sub_e(void *arg)
{
    int i;
    thread_t ret_thr;
    void *status;

    printf("E: In thread E...\n"); 
    printf("E: Join A thread\n"); 

    if(thr_join(thr_a,(thread_t *)&ret_thr, &status)) 
        fprintf(stderr,"thr_join Error\n"), exit(1);

    printf("E: A thread (%d) returned thread (%d) w/status %d\n", ret_thr, ret_thr, (int) status); 
    printf("E: Join B thread\n"); 

    if(thr_join(thr_b,(thread_t *)&ret_thr, &status)) 
        fprintf(stderr,"thr_join Error\n"), exit(1);

    printf("E: B thread (%d) returned thread (%d) w/status %d\n", thr_b, ret_thr, (int) status); 
    printf("E: Join C thread\n"); 

    if(thr_join(thr_c,(thread_t *)&ret_thr, &status)) 
        fprintf(stderr,"thr_join Error\n"), exit(1);

    printf("E: C thread (%d) returned thread (%d) w/status %d\n", thr_c, ret_thr, (int) status); 

    for (i=0;i<1000000*(int)thr_self();i++);

    printf("E: Thread exiting...\n"); 
    thr_exit((void *)44);
}

void *sub_f(void *arg)
{
    int i;

    printf("F: In thread F...\n"); 

    while (1) {
        for (i=0;i<10000000;i++);
        printf("F: Thread F is still running...\n"); 
    }
}

NERWous C Sample

The NERWous C version follows the original thread-reentrant version as closely as possible. Three global pel variables, PEL_a, PEL_b, PEL_c, are the NERWous C equivalent of the global thread_t variables, thr_a, thr_b, thr_c: They represent the first three tasks (A, B, C). The subsequent tasks (D, E, F) are then pel'led (i.e. created) from those first three tasks.
<pel>PEL_a, PEL_b, PEL_c;

void main() {
    PEL_b = <! name="PEL_B" mode=suspend>sub_b();
    PEL_a = <! name="PEL_A">sub_a(PEL_b);
    PEL_c = <! name="PEC_C">sub_c(pel);

    printf("Main created threads A:%d B:%d C:%d\n", PEL_a<id>, PEL_b<id>, PEL_c<id>); 
    printf("Main thread %d exiting...\n", pel<id>); 
}

int sub_a (<pel> pel_b) {
    printf("A: In thread A...\n"); 

    <pel>pel_d = <!>sub_d (pel_b);
    printf("A: Created thread D:%d\n", pel_d<id>); 

    /* process */
    for (int i=0;i<1000000*(int)pel<id>;i++);

    printf("A: Thread exiting...\n"); 
    return 77;
}

int sub_b () {
    printf("B: In thread B...\n"); 

    /* process */
    for (int i=0;i<1000000*(int)pel<id>;i++);

    printf("B: Thread exiting...\n"); 
    return 66;
}

int sub_c (<pel> pel_main) {
    printf("C: In thread C...\n"); 

    try { <pel>pel_f = <!>sub_f(); }
    catch (pel_f<FAILED>) { fprintf(stderr, "Can't create thread F\n"); return 1; }
    
    printf("C: Wait for main thread\n"); 
    <? ENDED>pel_main;
    printf("C: Main thread (%d) exits w/state %d\n", pel_main<id>, pel_main<state>); 

    /* process */
    for (int i=0;i<1000000*(int)pel<id>;i++);

    printf("C: Thread exiting...\n"); 
    return 88;
}

int sub_d (<pel> pel_b) {
    int ret;
    printf("D: In thread D...\n"); 

    try { <pel>pel_e = <!>sub_e (); }
    catch (pel_e<FAILED>) { fprintf(stderr,"Can't create thread E\n"), return 0; }
    printf("D: Created thread E:%d\n", pel_e<id>); 

    printf("D: Start B thread = %d\n", pel_b<id>);  
    <start>pel_b;

    printf("D: Wait for E thread\n");
    ret = <?>pel_e;
    printf("D: E thread (%d) returned value (%d) w/state %d\n", pel_e<id>, ret, pel_e<state>);

    /* process */
    for (inti=0;i<1000000*(int)pel<id>;i++);

    printf("D: Thread exiting...\n"); 
    return 55;
}

int sub_e()
{
    int ret;
    printf("E: In thread E...\n"); 

    printf("E: Wait for A thread\n"); 
    <update name="PEL_A">PEL_a;
    try { ret = <?>PEL_a; }
    catch ( PEL_a<FAILED> ) {
       fprintf(stderr,"wait Error\n");
       return(1);
    }
    printf("E: A thread (%d) returned value (%d) w/state %d\n", PEL_a<id>, ret, PEL_a<state>); 

    printf("E: Wait for B thread\n"); 
    <update name="PEL_B">PEL_b;
    try { ret = <?>PEL_b; }
    catch ( PEL_b<FAILED> ) {
       fprintf(stderr,"wait Error\n");
       return(1);
    }
    printf("E: B thread (%d) returned value (%d) w/state %d\n", PEL_b<id>, ret, PEL_b<state>); 
  
    printf("E: Wait for C thread\n"); 
    <update name="PEL_C">PEL_c;
    try { ret = <?>PEL_c; }
    catch ( PEL_c<FAILED> ) {
       fprintf(stderr,"wait Error\n");
       exit(1);
    }
    printf("E: C thread (%d) returned value (%d) w/state %d\n", PEL_c<id>, ret, PEL_c<state>); 

    /* process */
    for (int i=0;i<1000000*(int)pel<id>;i++);

    printf("E: Thread exiting...\n"); 
    return 44;
}

The main task pells the task B in suspend mode. The task B is created by main but does not run until task D (sub_d) invokes the pel operation start on task B. This is equivalent to the thr_continue library call in the thread-reentrant version.

The task A carries the pel presentation of task B in its pel argument pel_b. The main task initializes this argument with value from the global variable PEL_b at the time of pelling. Task A then transfers this value to task D so that it can start the suspended task B.

The task B is created and then immediately suspended by the main task. Once woken up (started by task D), it will spin a unique number of iterations based on its own task ID (pel<id>). The other tasks are doing the same spinning but using their own unique task ID.

The task C waits for the main task to complete by waiting on the return of that task (<?>pel_main). The return of a task is a read-only mel variable; being a mel, it allows waiting via the wait operator (<?>). This is equivalent to the thread library call thr_join which "suspends processing of the calling thread until the target thread completes".

The task D uses the pel operation start to start the suspended task B. The start operation is equivalent to the library function thr_continue used by the thread-reentrant version. This library call "resumes the execution of a suspended thread".

The task E makes use of the three global variables PEL_a, PEL_b, and PEL_c which are initialized all the way back in the main task. Based on the way NERWous C handles global variables, they are re-created in the context of the task E the first time that the sub_e code encounters them. However they are created un-initialized. One way to initialize them would be to carry their values from main to task A then task D then task E, via the import or import-file attribute. Since these global variables are pel variables, task E has another option -- which it uses here. Task E invokes the update operation on the pel variables in order to pull the necessary values of the task in real time. The update operation depends on finding the tasks. This is done via the name attribute, which the main task uniquely gives to the tasks that it pells.

An astute reader will notice that task E does not need to use global variables. Running an update operation on local pel variables will work just as well. The global variables are used to more closely match the thread-reentrant version which has them.


Previous Next Top