CS560 Lecture notes -- The Printer Simulation with CBThreads

  • Jim Plank
  • Directory: /home/plank/cs560/notes/CBT_PQ
  • Lecture notes: http://web.eecs.utk.edu/~jplank/plank/classes/cs560/560/notes/CBT_PQ
  • Latest revision: Fri Feb 12 14:25:41 EST 2010
    In this lecture, we convert the printer queue simulation to use the cbthread library. First, we augment printqsim.h so that the Spq struct has a job pointer. That simplifies life. Next, since the two procedures submit_job() and get_print_job() are potentially blocking, we have them include a function that should be called on s when they unblock:

    typedef struct {
      int jobsize;
      int userid;
      int jobid;
    } Job;
    
    typedef struct {
      int nusers;
      int nprinters;
      int arrtime;
      int maxpages;
      int bufsize;
      int nevents;
      int starttime;
      int id;
      int event;
      Job *job;
      void *v;
    } Spq;
    
    extern void initialize_v(Spq *);
    
    /* When this is done, it will call function(s) as a continuation */
    extern void submit_job(Spq *s, Job *j, void (*function)());  
    
    /* When this is done, it will call function(s) as a continuation,
       and  s->job will be set to be the job. */
    extern void get_print_job(Spq *s, void (*function)());
    


    The code in printqsim.c breaks up each user and printer thread into three procedures. Let's look at the user threads:

    void user_ready_to_print(Spq *s);
    void user_job_submitted(Spq *s);
    
    void user_thread(Spq *s)
    {
      int sleeptime;
    
      if (s->event == s->nevents) return;
      sleeptime = random()%(s->arrtime*2) + 1;
      printf("%4d: user %2d/%03d: Sleeping for %2d seconds\n", 
              time(0)-s->starttime, s->id, s->event, sleeptime);
      fflush(stdout);
      cbthread_sleep(sleeptime, user_ready_to_print, s);
    }
    
    void user_ready_to_print(Spq *s)
    {
      Job *j;
    
      j = (Job *) malloc(sizeof(Job));
      j->jobsize = (random()%s->maxpages) + 1;
      j->userid = s->id;
      j->jobid = s->event;
      printf("%4d: user %2d/%03d: Submitting a job with size %d\n", 
            time(0)-s->starttime, s->id, s->event, j->jobsize);
      fflush(stdout);
      submit_job(s, j, user_job_submitted);
    }
    
    void user_job_submitted(Spq *s)
    {
      s->event++;
      user_thread(s);
    }
    

    There are three user-thread procedures: user_thread(), user_ready_to_print() and user_job_submitted(). Since the first thing that a user does is sleep, this is done in user_thread(). Cbthread_sleep() is a blocking call, so the continuation user_ready_to_print() is called when the sleeping is over. That procedure generates a job and calls submit_job(), with user_job_submitted() as a continuation. The final continuation simply updates the event and repeats the user_thread().

    The printer threads are broken up similarly.


    Solution #1

    Obviously, the solution must include a bounded buffer and two semaphores: one for the jobs and one for buffer slots. However, things are a little tricky, because the first thing that submit_job() has to do is call P() on the slot semaphore, and the first thing that get_print_job() has to do is call P() on the job semaphore. Thus, they both have to block, but there is information that has to be passed onto the continuation.

    The first solution is a standard solution to this problem -- add some information and pass that to the continuation. This code is in ps_cbt1.c. Specifically, I define a struct called Mycont, which includes the function() parameter, the Spq, a job and a state. Then, I implement submit_job() by allocating one of these Mycont's and calling real_submit_job() on it. real_submit_job() does all the work. It first calls P() on the slot semaphore, using itself as the continuation. When it is called a second time, it puts the job into the slot, deallocates the Mycont and calls the exit function on s.

    Get_print_job() works in the exact same way. It's a little clunky, but it works.

    #define START 0
    #define GOT_JOB 1
    #define GOT_SLOT 2
    
    typedef struct {
      void (*func)();
      Spq *s;
      Job *job;
      int state;
    } Mycont;
    
    void real_submit_job(Mycont *mc);
    void real_get_print_job(Mycont *mc);
    
    typedef struct {
      cbthread_gsem job_sem;
      cbthread_gsem slot_sem;
      Job **buffer;
      int head;
      int tail;
    } Info;
    
    void initialize_v(Spq *s)
    {
      Info *info;
    
      info = (Info *) malloc(sizeof(Info));
      info->job_sem = cbthread_make_gsem(0);
      info->slot_sem = cbthread_make_gsem(s->bufsize);
      info->buffer = (Job **) malloc(sizeof(Job *)*s->bufsize);
      info->head = 0;
      info->tail = 0;
      s->v = (void *) info;
    }
    
    void submit_job(Spq *s, Job *j, void (*function)())
    {
      Mycont *mc;
    
      mc = (Mycont *) malloc(sizeof(Mycont));
      mc->func = function;
      mc->job = j;
      mc->s = s;
      mc->state = START;
      
      real_submit_job(mc);
    }
    
    void real_submit_job(Mycont *mc) 
    {
      Info *info;
      Spq *s;
      void (*func)();
    
      s = mc->s;
      info = (Info *) s->v;
    
      if (mc->state == START) {
        mc->state = GOT_SLOT;
        cbthread_gsem_P(info->slot_sem, real_submit_job, mc);
    
      } else if (mc->state == GOT_SLOT) {
        info->buffer[info->tail] = mc->job;
        info->tail = (info->tail + 1) % s->bufsize;
        cbthread_gsem_V(info->job_sem);
        func = mc->func;
        free(mc);
        (*func)(s);
        cbthread_exit();
      }
    }
    
    void get_print_job(Spq *s, void (*function)())
    {
      Mycont *mc;
    
      mc = (Mycont *) malloc(sizeof(Mycont));
      mc->func = function;
      mc->s = s;
      mc->state = START;
      
      real_get_print_job(mc);
    }
    
    void real_get_print_job(Mycont *mc)
    {
      Spq *s;
      Info *info;
      void (*func)();
    
      s = mc->s;
      info = (Info *) s->v;
    
      if (mc->state == START) {
        mc->state = GOT_JOB;
        cbthread_gsem_P(info->job_sem, real_get_print_job, mc);
      } else if (mc->state == GOT_JOB) {
        s->job = info->buffer[info->head];
        info->head = (info->head + 1) % s->bufsize;
        cbthread_gsem_V(info->slot_sem);
        func = mc->func;
        free(mc);
        (*func)(s);
        cbthread_exit();
      }
    }
    


    Solution #2

    I give a second solution to show a different way of handling the information. The code is in ps_cbt2.c. Here I add arrays of user and printer continuation functions to my Info struct, and an array of job pointers as well. Each user i can use user_funcs[i] and user_jobs[i], and each printer i can use printer_funcs[i]. This way all malloc() calls are in the initialization phase, and each procedure can store continuation information in the Info struct. Here's the info struct and initialize_v() calls:

    typedef void (*func)();
    
    typedef struct {
      cbthread_gsem job_sem;
      cbthread_gsem slot_sem;
      Job **buffer;
      int head;
      int tail;
      func *user_funcs;
      Job **user_jobs;
      func *printer_funcs;
    } Info;
    
    void initialize_v(Spq *s)
    {
      Info *info;
    
      info = (Info *) malloc(sizeof(Info));
      info->job_sem = cbthread_make_gsem(0);
      info->slot_sem = cbthread_make_gsem(s->bufsize);
      info->buffer = (Job **) malloc(sizeof(Job *)*s->bufsize);
      info->user_funcs = (func *) malloc(sizeof(func)*s->nusers);
      info->printer_funcs = (func *) malloc(sizeof(func)*s->nprinters);
      info->user_jobs = (Job **) malloc(sizeof(Job *)*s->nusers);
      info->head = 0;
      info->tail = 0;
      s->v = (void *) info;
    }
    

    I break up submit_job() into two procedures -- one before the P() call and one afterwards. The job and function that are parameters to submit_job() are stored in the user_jobs and user_funcs arrays, so that submit_job_slot_ready() may use them after the P() call unblocks. At the end of submit_job_slot_ready(), I call the user's function using cbthread_yield(). That's just to demonstrate it. I could have called the function directly as in Solution #1.

    Get_print_job() is broken up similarly. I personally like the second solution better than the first, because I don't have to call malloc() and free() to help with control flow, but that's just my preference. I figured it would be good for y'all to see both.

    void submit_job(Spq *s, Job *j, void (*function)())
    {
      Info *info;
    
      info = (Info *) s->v;
      info->user_jobs[s->id] = j;
      info->user_funcs[s->id] = function;
      cbthread_gsem_P(info->slot_sem, submit_job_slot_ready, s);
    }
    
    void submit_job_slot_ready(Spq *s)
    {
      Info *info;
    
      info = (Info *) s->v;
      info->buffer[info->tail] = info->user_jobs[s->id];
      info->tail = (info->tail + 1) % s->bufsize;
      cbthread_gsem_V(info->job_sem);
      cbthread_yield(info->user_funcs[s->id], s);
    }
    
    void get_print_job(Spq *s, void (*function)())
    {
      Info *info;
    
      info = (Info *) s->v;
      info->printer_funcs[s->id] = function;
      cbthread_gsem_P(info->job_sem, job_ready, s);
    }
    
    void job_ready(Spq *s)
    {
      Info *info;
    
      info = (Info *) s->v;
      s->job = info->buffer[info->head];
      info->head = (info->head + 1) % s->bufsize;
      cbthread_gsem_V(info->slot_sem);
      cbthread_yield(info->printer_funcs[s->id], s);
    }
    


    Solution #3

    Rather than use the semaphores as counters, you can simply use them as blocking primitives, similar to condition variables. The code in ps_cbt3.c does this. Now the code is no longer splitting the loop into two procedures. There are two procedures for submit_job() and get_print_job(), but one simply sets up the data, and the other performs the continuation-based loop.

    Ask yourself whether the program wakes up too many or too few threads in the various V() calls. Could you remove the if statements ahead of them? If so, would the program run incorrectly or more slowly (the answer is more slowly, but correctly).

    typedef void (*func)();
    
    typedef struct {
      cbthread_gsem user_sem;
      cbthread_gsem printer_sem;
      Job **buffer;
      int head;
      int tail;
      int njobs;
      func *user_funcs;
      Job **user_jobs;
      func *printer_funcs;
    } Info;
    
    void initialize_v(Spq *s)
    {
      Info *info;
    
      info = (Info *) malloc(sizeof(Info));
      info->user_sem = cbthread_make_gsem(0);
      info->printer_sem = cbthread_make_gsem(0);
      info->buffer = (Job **) malloc(sizeof(Job *)*s->bufsize);
      info->user_funcs = (func *) malloc(sizeof(func)*s->nusers);
      info->printer_funcs = (func *) malloc(sizeof(func)*s->nprinters);
      info->user_jobs = (Job **) malloc(sizeof(Job *)*s->nusers);
      info->head = 0;
      info->tail = 0;
      info->njobs = 0;
      s->v = (void *) info;
    }
    
    void submit_job_cont(Spq *s)
    {
      Info *info;
    
      info = (Info *) s->v;
      if (info->njobs != s->bufsize) {
        info->buffer[info->tail] = info->user_jobs[s->id];
        info->tail = (info->tail + 1) % s->bufsize;
        info->njobs++;
        if (cbthread_gsem_getval(info->printer_sem) < 0) cbthread_gsem_V(info->printer_sem);
        (*info->user_funcs[s->id])(s);
        cbthread_exit();
      } else {
        cbthread_gsem_P(info->user_sem, submit_job_cont, s);
      }
    }
    
    void submit_job(Spq *s, Job *j, void (*function)())
    {
      Info *info;
    
      info = (Info *) s->v;
      info->user_funcs[s->id] = function;
      info->user_jobs[s->id] = j;
      submit_job_cont(s);
    }
    
    void get_print_job_cont(Spq *s)
    {
      Info *info;
    
      info = (Info *) s->v;
      if (info->njobs == 0) {
        cbthread_gsem_P(info->printer_sem, get_print_job_cont, s);
      } else {
        s->job = info->buffer[info->head];
        info->head = (info->head + 1) % s->bufsize;
        info->njobs--;
        if (cbthread_gsem_getval(info->user_sem) < 0) cbthread_gsem_V(info->user_sem);
        (*info->printer_funcs[s->id])(s);
      }
    }
    void get_print_job(Spq *s, void (*function)())
    {
      Info *info;
    
      info = (Info *) s->v;
      info->printer_funcs[s->id] = function;
      get_print_job_cont(s);
    }