C460 Lecture notes -- Pseudo-Threads Lecture #2

  • Jim Plank
  • CS460: Operating Systems
  • Directory: /blugreen/homes/plank/cs460/notes/PThreads2
  • Lecture notes -- html: http://web.eecs.utk.edu/~jplank/plank/classes/cs460/460/notes/PThreads2/lecture.html

    General Semaphores

    A general semaphore is a data structure for thread/process synchronization. It has a value that the user can access, plus other stuff that the user cannot access:
    typedef struct sem {
      int value;
      other_stuff
    } *Sem;
    
    There are two actions defined on semaphores: P(Sem s) and V(Sem s). (The book calls them wait() and signal()).

    General Semaphores in the PT-library

    The pt library implements general semaphores as its main synchronization primitive. The interface to these are:
      typedef void *Gsem;
      
      Gsem make_gsem(initval)
      int initval;
    
      void kill_gsem(g)
      Gsem g;
    
      int gsem_getval(g)
      Gsem g;
    
      void gsem_P(g, function, arg)
      Gsem g;
      void (*function)();
      void *arg;
    
      void gsem_V(g)
      Gsem g;
    
    Make_gsem() and kill_gsem() create and destroy semaphores respectively. gsem_getval() returns the value of the semaphore. The reason that Gsem is a (void *) and not a struct with a val field is that you should not be allowed to set the value of the semaphore except when it is created. Thus, it is a (void *) and you can only access its value through the procedure gsem_getval().

    gsem_P() and gsem_V() are standard P() and V() operations. The only tricky thing is that since P() is a potentially blocking call, you must pass it a continuation, and it will never return. It simply calls the continuation when it unblocks.

    Threads that block on a gsem_P() call will unblock in FIFO order. You may make use of this fact if you need to do so to eliminate starvation.

    It is an error to call kill_gsem() on a semaphore that has blocked threads.


    Printqsim revisited

    Now, we revisit the printqsim application from Thread lecture #4. We first convert printqsim.h and printqsim.c so that they work with the pt-library. Then, we show a null solution, and a solution to the printer queue simulation that uses general semaphores.

    First, look at printqsim.h. There are a few differences from before. First, we define some states (START, etc), and we include a state field in the Spq struct. Also, we include an integer nsofar and a job j in the Spq struct. Nsofar is the number of events so far (either for the user or the printer, and j is the current job that the printer is printing.

    The other big change is that since submit_job() and get_print_job() are now potentially blocking calls, they will not return, and instead must be passed continuations. Additionally, since get_print_job() does not return, it cannot pass the job to print in its return value. Instead, it sets the j field of the printer's spq struct.

    Now, look at printqsim.c. First, check out the main() routine. It sets the new fields (setting the state of all threads to START) and then forks off the threads using pt_fork(). It then exits using pt_exit(). Had it simply returned, the program would have exited.

    Now, look at the user_thread() routine. This has been rewritten to use a continuation-based style. If the thread is in the START state, then it must sleep for a random number of seconds before printing. This is done with the pt_sleep() call. Since pt_sleep() is a blocking call, it does not return. Instead it is passed a continuation to user_thread() so that when it is done, user_thread() gets called again. However, the state is first set to SLEEPING so that when user_thread() is called again, it knows what to do.

    What is does is now submit the job for printing by calling submit_job(). Since submit_job() may block, it must be passed a continuation, and like before, the continuation specifies to call user_thread() again. However, this time the state is set to PRINTING. When user_thread() is called again, it will know that the submission to the print queue is complete, and it will increment nsofar and start again.

    printer_thread() is written in a similar style, only now the states are START, GETTING_JOB and PRINTING. When it is called with a state of GETTING_JOB, we know that a new job to print is in s->j, and we print it.


    The null solution

    Now, the null solution once again is in ps1.c. Note that instead of returning, submit_job() and get_print_job() call their continuations. That is because the calling processes do not expect them to return. Try it out and you will see output like ps1 in the Thread lecture #4.

    A solution using general semaphores

    For this problem, general semaphores provide an extremely convenient solution. The code is in ps5.c. Instead of having a monitor and two condition variables, we just have two general semaphores, one (njobs) for the number of jobs in the buffer, and one (nslots) for the number of buffer slots available. We initialize njobs to zero, and nslots to bufsize.

    Now, the structure for submit_job() is very straightforward. It allocates a slot by calling P() on nslots, enters the job, and then calls V() on njobs to awake any printers that may be waiting to print a job.

    Unfortunately, the continuation-based structure of the pt-library makes this a little cumbersome. To call gsem_P(), we have to pass a continuation to the code that finishes submit_job(). I do this by allocating a Cont struct and passing this as the argument to submit_job_cont(). When submit_job_cont() is called, it finishes submit_job and then calls pt_yield() with the original continuation passed to submit_job(). This will give control back to user_thread(). Note that I could have just called f(a).

    get_printer_job() is structured in the same manner.

    Give it a try and run it -- you'll see that everything works. Note that in the pthreads solution we had to protect all the code in submit_job() and get_printer_job() with a mutex. This is because the pthreads system is preemptive. Since the pt-library is non-preemptive, we don't have to worry about our code getting preempted -- threads only switch at blocking points (e.g. gsem_P(), pt_yield(), pt_sleep(), etc).


    A cleaner solution?

    Finally, take a look at ps6.c. I think this is a little cleaner, but you may disagree. Instead of allocating a (Cont) for each gsem_P() call, we put the continuation into the buffer. I have nusers+nprinters function pointers in b->f and nusers+nprinters args in b->a. User i uses b->f[i], and printer i uses b->f[nusers+i]. This way, you don't have the extra struct, and you don't have to call malloc()/free().

    I'll let you decide which of these you like better.