C460 Lecture notes -- Pseudo-Threads Lecture #1

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

    The Pseudo-Threads Library

    Our last topic in threads for a little bit will be the ``pseudo-threads'' library. This is a simple, bizarre (and in my opinion, pretty cool) library that implements continuation-based, non-preemptive threads. You'll find the pseudo-threads extremely convenient for OS programming, where the flow of control makes threads a natural programming abstraction, but you don't need preemption.

    Since the pseudo-threads library is non-preemptive, threads only lose control of the CPU when they explicitly block. You've seen that before. However, what does ``continuation-based'' mean? What it means is that whenever a thread makes a potentially blocking library call, that call will not return. Instead, all such calls require the thread to pass a pointer to a procedure and a (void *) pointer. This is called a ``continuation''. When the thread unblocks (or if the thread never was supposed to block), instead of returning to the calling procedure, the continuation is invoked -- the procedure is called with the (void *) pointer as its argument.

    Compiling/linking

    To use the pseudo-threads library (which I'll heretofore call the pt library), you include the file /blugreen/homes/plank/cs460/include/pt.h, and then compile with pt.o, dlist.o and rb.o from /blugreen/homes/plank/cs460/objs.

    Forking and joining

    The fork()/join() procedure calls of the pt library are:
      void *pt_fork(function, arg)
      void (*function)();
      void *arg;
    
      pt_join(void *thread_id, function, arg)
      void *thread_id;
      void (*function)();
      void *arg;
    
      pt_joinall(function, arg)
      void (*function)();
      void *arg;
    
      pt_exit()
    
    pt_fork() creates a new thread, which will execute function(arg), and puts it on the ready queue. It returns the thread id of the thread (which is a (void *). Since the thread system is non-preemptive, the thread that calls pt_fork() does not lose the CPU after the call. The new thread will only execute once the calling thread blocks.

    pt_join() is a blocking call. It blocks and waits for the thread thread_id to complete. When that thread completes, the thread calling pt_join() is put on the ready queue, and when it is executed, its continuation (function(arg)) is executed. pt_join never returns. When its continuation returns or calls pt_exit(), its thread is terminated.

    pt_joinall() is also a blocking call. It waits until there are no more threads in the system that are either ready to run or sleeping. When that happens, its continuation is executed. Note that it will also run if all other threads are blocked on semaphores, because in such a case, there is no way for those threads to become ready. pt_joinall() is included because it is more convenient to use than pt_join() when you just want to block until all threads are done.

    pt_exit() makes the thread exit. It is just like pthread_exit() in the pthreads package, except there is no return value. As in the pthreads package, in most cases, just calling return does the same thing, but sometimes you want to make sure. For example, if you don't want your main() program to return because that will exit the process, and you don't want to call pt_join() or pt_joinall(), then you can have it call pt_exit().


    Simple examples: hello world

    hw.c is a simple example of forking off a thread that prints ``hello world.'' It is included below:
    #include 
    #include "pt.h"
    
    extern exit();
    
    hw()
    {
      printf("Hello World\n");
    }
    
    main()
    {
      void *t;
    
      t = pt_fork(hw, NULL);
      pt_join(t, exit, 0);
    } 
    
    This is pretty simple. Note how pt_join calls exit(0) when the hw() thread returns. One important thing to note is that if the main() program returns, the process will exit, and no other threads will run. Thus, you have to make the main thread block in order to make the other threads run. In hw.c, this is done with the pt_join() call.

    hw2.c is like hw.c except that it defines a procedure to call upon joining, and it prints out a few statements. See if you can trace how it gets its output:

    UNIX> hw2
    Main thread -- between pt_fork and pt_join
    Hello World
    In do_exit -- returning
    No more threads to run
    UNIX>
    
    Note that the printf("Main thread -- do I ever get here?\n"); line never gets executed. This is because the pt_join() call does not return. Instead, do_exit(NULL) is called, and when do_exit() returns, there are no more threads to run, so the system prints out "No more threads to run" and exits. If you do not exit using exit(), then the threads system will exit if there are no threads to run, and no threads sleeping. Before it exits, it prints "No more threads to run".

    hw3.c forks off five hw() threads, and then calls pt_joinall() to exit when they are all done.


    Three Simple Questions

    1. Q: What happens if you delete the pt_joinall() line in hw3.c?

      A: If the main() program exits without blocking, the process exits. To run the threads, the main() program must make a blocking call. Therefore, if you remove the pt_joinall() line in hw3.c, the program will exit without printing anything. Try it.

    2. Q: Suppose I change the pt_joinall() line in hw3.c to be pt_join(t,exit,0). What is going to happen?

      A: Now, the main() program blocks, so the thread system will indeed start running. The problem is that we're only calling pt_join() on the last thread (#4). We've lost the thread id's of the others during the for loop. So, once thread #4 finishes, the continuation exit(0) will be put on the ready queue. At that point, all the other threads will be done, so it will be executed, and the program will exit. In other words, the output will be the exact same as hw3.

    3. Q: Suppose I change the pt_joinall() line in hw3.c to be pt_join(t,hw,10). What is going to happen?

      A: Now, when thread #4 is done, the pt_join() call causes the continuation hw(10) to be put on the ready queue. As it will be the only thread, it will execute, and ``Thread 10: Hello World'' will print out. When hw(10) returns, there will be no more threads to run. Since no pt_joinall() continuation has been specified, the threads system will print out ``No more threads to run'' and the system will exit. The whole output will be:

      Thread 0: Hello World
      Thread 1: Hello World
      Thread 2: Hello World
      Thread 3: Hello World
      Thread 4: Hello World
      Thread 10: Hello World
      No more threads to run
      

    Making threads sleep

    The pt library provides its own sleeping procedure for threads. This is because if you call sleep(), the whole process will sleep, and no threads will run. This isn't a problem with pthreads in Solaris, because it can bind user-level threads to system-level threads. With the pt library, there is only one system-level thread, to which all user-level threads are bound, so any system call will block the entire process.

    Pt_sleep() is the way you make a thread sleep in the pt-library without having the other threads sleep too.

      pt_sleep(s, function, arg)
      int s;
      void (*function)();
      void *arg;
    
    This specifies that after s seconds, the continuation function(arg) will be put on the end of the ready queue. If s is less than or equal to zero, this simply puts the continuation function(arg) at the end of the ready queue and runs the next thread. Note that pt_sleep() is a blocking call, and therefore does not return.

    pt_yield() is equivalent to pt_sleep() where s is zero:

      pt_yeild(function, arg)
      void (*function)();
      void *arg;
    
    To test this out, look at sleeptest.c. This program forks off five threads. Each thread prints out its id, a counter, and how many seconds it is going to sleep. This is a random number between zero and 4. Then it calls pt_sleep() for that many seconds. When the thread is done sleeping, it calls thread(t) again -- thus it iterates forever printing, incrementing the counter and sleeping.

    Try it out. Sample output is in sleeptest.out. It may be hard to figure out if everything is working right. To help out, grep to get the output of a single thread:

    UNIX> grep 'Thread 0' sleeptest.out
       0: Thread 0.  i = 0.  Sleeping for 0 seconds
       0: Thread 0.  i = 0.  Sleeping for 1 seconds
       1: Thread 0.  i = 0.  Sleeping for 1 seconds
       2: Thread 0.  i = 0.  Sleeping for 0 seconds
       2: Thread 0.  i = 0.  Sleeping for 0 seconds
       2: Thread 0.  i = 0.  Sleeping for 4 seconds
       6: Thread 0.  i = 0.  Sleeping for 2 seconds
       8: Thread 0.  i = 0.  Sleeping for 0 seconds
       8: Thread 0.  i = 0.  Sleeping for 1 seconds
       9: Thread 0.  i = 0.  Sleeping for 0 seconds
       9: Thread 0.  i = 0.  Sleeping for 4 seconds
      13: Thread 0.  i = 0.  Sleeping for 1 seconds
      14: Thread 0.  i = 0.  Sleeping for 3 seconds
      17: Thread 0.  i = 0.  Sleeping for 2 seconds
      19: Thread 0.  i = 0.  Sleeping for 3 seconds
      23: Thread 0.  i = 0.  Sleeping for 3 seconds
    UNIX>
    
    You should see that everything looks good. Four seconds did pass when it slept for 3 seconds at the 19 second mark, but that's ok -- like Unix's sleep() call, this is a lower bound, not an upper bound.