Skip to content

Latest commit

 

History

History
222 lines (196 loc) · 6.56 KB

thread_tutorial.md

File metadata and controls

222 lines (196 loc) · 6.56 KB

Yielding C Fun Thread Example Tutorial

This tutorial goes through how Yielding C Fun can be used to simulate multi-threading in a single thread. You can find the source code that we use in the tutorial below or in ../test/examples/thread_example.c.

#include <stdio.h>
#include <stdlib.h>

#define YCF_YIELD_NO_REDS()

static int f_2(char* name, int n) {
  for(int y = 0; y < 2; y++) {
    printf("%s f_2: y=%d\n", name, y);
  }
  return n*2;
}

static void f_1(char* name, int x) {
  YCF_YIELD_NO_REDS();
  while (x > 0) {
    int f_2_ret = f_2(name, x);
    printf("%s f_1: x=%d f_2_ret=%d\n", name, x, f_2_ret);
    x--;
  }
  printf("%s f_1: DONE\n", name);
}

static void* ycf_alloc(size_t size, void* context){
  (void)context;
  return malloc(size);
}

static void ycf_free(void* data, void* context){
  (void)context;
  free(data);
}

int main( int argc, const char* argv[] ) {
#ifdef YCF_YIELD_CODE_GENERATED
  long t1_nr_of_reds = 1;
  void* t1_state = NULL;
  long t2_nr_of_reds = 2;
  void* t2_state = NULL;
  long t3_nr_of_reds = 1000;
  void* t3_state = NULL;
  /* Start t1, t2  and t3*/
  f_1_ycf_gen_yielding(&t1_nr_of_reds, &t1_state, NULL,
                       ycf_alloc, ycf_free, NULL, 0, NULL,
                       "t1", 2);
  f_1_ycf_gen_yielding(&t2_nr_of_reds, &t2_state, NULL,
                       ycf_alloc, ycf_free, NULL, 0, NULL,
                       "t2", 4);
  f_1_ycf_gen_yielding(&t3_nr_of_reds, &t3_state, NULL,
                       ycf_alloc, ycf_free, NULL, 0, NULL,
                       "t3", 2);
  printf("THREADS STARTED\n");
  /* Execute t1, t2 and t3*/
  while (t1_state != NULL ||
         t2_state != NULL ||
         t3_state != NULL) {
    t1_nr_of_reds = 1;
    t2_nr_of_reds = 2;
    if (t1_state != NULL) {
      printf("SCHEDULING THREAD: t1\n");
      f_1_ycf_gen_continue(&t1_nr_of_reds, &t1_state, NULL);
      if (t1_state == NULL) {
        printf("THREAD t1 DONE (number of reductions left = %ld)\n",
               t1_nr_of_reds);
      }
    }
    if (t2_state != NULL) {
      printf("SCHEDULING THREAD: t2\n");
      f_1_ycf_gen_continue(&t2_nr_of_reds, &t2_state, NULL);
      if (t2_state == NULL) {
        printf("THREAD t2 DONE (number of reductions left = %ld)\n",
               t2_nr_of_reds);
      }
    }
    if (t3_state != NULL) {
      printf("SCHEDULING THREAD: t3\n");
      f_1_ycf_gen_continue(&t3_nr_of_reds, &t3_state, NULL);
      if (t3_state == NULL) {
        printf("THREAD t3 DONE (number of reductions left = %ld)\n",
               t3_nr_of_reds);
      }
    }
  }
#endif
  (void)f_1;
  printf("DONE\n");
  return 0;
}

To run this example, you need to compile YCF itself, if you have not done so already:

cd directory_where_the_ycf_source_code_is_located
YCF_ROOT=`pwd`
make

Now, you can transform test/examples/thread_example.c by executing the following command:

"$YCF_ROOT"/bin/yielding_c_fun.bin \
        -f f_1 -f f_2 \
        -output_file_name modified_thread_example.c \
        "$YCF_ROOT"/test/examples/thread_example.c

A new file should now exist in your current directory called modified_thread_example.c. This file contains a transformed version of test/examples/thread_example.c. The parameters -f f_1 and -f f_2 tells YCF to generate yieldable versions of the functions named f_1 and f_2.

Before you inspect the generated file to see what the tool has done, it is smart to format the generated source code with clang-format or some other tool to make the code more readable:

clang-format -i modified_thread_example.c

You can now open the generated source code in your favorite editor:

emacs modified_thread_example.c &

The yieldable functions get the suffix _ycf_gen_yielding. For example, the yielding version of f_1 is called f_1_ycf_gen_yielding. As you can see, several parameters have been added to the yieldable versions of f_1. We explain the first two of these parameters here. The main documentation of YCF explains the rest of the parameters. The first parameter is an input/output parameter that tells the yieldable function how many reductions that can be "consumed" before it yields. The first parameter can also be used by the caller to get the number of reductions that have been consumed. The second parameter is also an input/output parameter that should be a pointer to a NULL pointer when the yieldable function is first started. The pointer that is pointed to by the second parameter is set to a value which is different from NULL when the function yields.

Let us now compile and run the generated source code so we can see what is happening:

cc -g modified_thread_example.c
$ ./a.out
THREADS STARTED
SCHEDULING THREAD: t1
SCHEDULING THREAD: t2
SCHEDULING THREAD: t3
t3 f_2: y=0
t3 f_2: y=1
t3 f_1: x=2 f_2_ret=4
t3 f_2: y=0
t3 f_2: y=1
t3 f_1: x=1 f_2_ret=2
t3 f_1: DONE
THREAD t3 DONE (number of reductions left = 994)
SCHEDULING THREAD: t1
SCHEDULING THREAD: t2
t2 f_2: y=0
t2 f_2: y=1
t2 f_1: x=4 f_2_ret=8
SCHEDULING THREAD: t1
t1 f_2: y=0
SCHEDULING THREAD: t2
t2 f_2: y=0
SCHEDULING THREAD: t1
t1 f_2: y=1
t1 f_1: x=2 f_2_ret=4
SCHEDULING THREAD: t2
t2 f_2: y=1
t2 f_1: x=3 f_2_ret=6
SCHEDULING THREAD: t1
SCHEDULING THREAD: t2
t2 f_2: y=0
t2 f_2: y=1
t2 f_1: x=2 f_2_ret=4
SCHEDULING THREAD: t1
t1 f_2: y=0
SCHEDULING THREAD: t2
t2 f_2: y=0
SCHEDULING THREAD: t1
t1 f_2: y=1
t1 f_1: x=1 f_2_ret=2
t1 f_1: DONE
THREAD t1 DONE (number of reductions left = 1)
SCHEDULING THREAD: t2
t2 f_2: y=1
t2 f_1: x=1 f_2_ret=2
t2 f_1: DONE
THREAD t2 DONE (number of reductions left = 2)
DONE
$

We can see that all three calls to the yieldable version of f_1 yield before printing anything. This is due to the YCF_YIELD_NO_REDS() statement at the beginning of f_1's body. We can also see that the "thread" named t3 runs to completion without yielding when we call f_1_ycf_gen_continue with t3_state in the while loop and that t3 consumed (1000-994=6) reductions. This is expected as we gave t3 1000 reductions when we started it with the f_1_ycf_gen_yielding function. The "threads" t1 and t2 get much fewer reductions (i.e., 1 and 2) each time they get to execute and they are therefore interleaved.

You can now experiment with other transformation options (e.g., -frec and -fnoauto) and the special statements that you can use inside functions that can yield. All these options and statements are documented here.

The best way to figure out how YCF works under the hood is probably to step through the transformed program in a debugger.

Good Luck!