#define RUNNING 0
#define READY 1
#define BLOCKED 2
#define SLEEPING 3
#define ZOMBIE 4
#define JOINING 5
typedef struct thread {
void (*function)();
void *arg;
int state;
struct thread *joiner;
} Thread;
If the thread is currently running, its state is RUNNING.
Otherwise, it is one of the following:
The joiner field contains a pointer to a thread that has called pt_join() on this thread. If there is no such thread, then the joiner field is NULL.
Thread id's are merely pointers to the thread's Thread struct. There is a global variable pt_self that points to the currently executing thread. It should always be the case that pt_self->state is RUNNING.
Other global variables are the Sleepq, Joinall, thebuf and first_time. These will be explained later. Here are all the type declarations for the global variables:
Thread *pt_self = NULL; static Dlist Readyq = NULL; static Rb_node Sleepq = NULL; static Thread *Joinall = NULL; static int first_time = 1; static jmp_buf thebuf;Note that the only non-static global variable is pt_self because it is the only one that we let users use.
Whenever a thread routine is invoked, it first tests to see if Readyq is NULL. If so, the threads system has not been initialized yet. At this point, pt_initialize() is called to initialize the state of the system. This initialization is straightforward: The Readyq and Sleepq are initialized to be empty, and a new Thread struct is created for the currently running thread. This is put into pt_self. Note that the function and arg fields of pt_self are not touched. This is because the thread is currently running -- no continuation is necessary.
static pt_initialize()
{
if (Readyq != NULL) {
fprintf(stderr, "PT: Called pt_initialize twice\n");
exit(1);
}
Readyq = make_dl();
Sleepq = make_rb();
pt_self = (Thread *) malloc(sizeof(Thread));
pt_self->state = RUNNING;
pt_self->joiner = NULL;
}
void *pt_fork(function, arg)
void (*function)();
void *arg;
{
Thread *p;
if (Readyq == NULL) pt_initialize();
p = (Thread *) malloc(sizeof(Thread));
p->state = READY;
p->function = function;
p->arg = arg;
p->joiner = NULL;
dl_insert_b(Readyq, p);
return (void *) p;
}
It is assumed that the blocking thread has already set its state appropriately, and it has stored itself in the proper data structures. For example, if the thread has blocked on a semaphore, it is assumed that it has put itself into the semaphore's blocked threads queue and set its state to BLOCKING. Therefore block_myself does not have to do any bookkeeping on the currently blocked thread.
A very simple strategy for block_myself() would be to take a thread off the ready queue, set its state to RUNNING, put it into pt_self and call the continuation. The problem with this is stack space. If we keep recursively calling continuations from block_myself(), our stack may grow without bounds, and one of the neat features of the pt library is its stacklessness.
The solution to this problem is to use setjmp()/longjmp(). Specifically, the first time that block_myself() is called, it calls setjmp(thebuf). Whenever it is called again, it calls longjmp(thebuf). This pops off all stack frames currently above the first call to block_myself(), and is exactly what we need. Thus, whenever a thread blocks, it calls longjmp() to pop all its frames off the stack. This is how we get ``stackless'' threads.
static block_myself()
{
/* Variable declarations */
if (Readyq == NULL) pt_initialize();
if (first_time) {
first_time = 0;
setjmp(thebuf);
} else {
longjmp(thebuf, 1);
}
...
Now, the code for taking a thread off the ready queue and running it is
straightforward:
...
if (!dl_empty(Readyq)) {
d = Readyq->flink;
p = (Thread *) d->val;
dl_delete_node(d);
pt_self = p;
p->state = RUNNING;
(*p->function)(p->arg);
...
Note that I don't show what happens when a thread returns or when there
are no threads left in the ready queue. I'll get to that later.
typedef struct gsem {
int val;
Dlist queue;
} *Gsem;
make_gsem() is straightforward. It allocates a Gsem struct, initializes its value from its argument, creates an empty dlist for queue, and returns the Gsem to the user as a (void *):
void *make_gsem(initval)
int initval;
{
Gsem g;
if (initval < 0) {
fprintf(stderr, "make_gsem: initval < 0 (%d)\n", initval);
exit(1);
}
g = (Gsem) malloc(sizeof(struct gsem));
g->val = initval;
g->queue = make_dl();
return g;
}
gsem_P() is a potentially blocking call, so it cannot return.
Instead, it sets up the system to call its continuation when it is done
being blocked. Here's exactly how it works. First the value of the semaphore
is decremented. If that value is less than zero, the thread must be
blocked. Therefore, the continuation in pt_self is set to the
arguments of gsem_P(), and pt_self is inserted into the
queue. Then block_myself() is called, which will
execute the first thread on the ready queue. This is the first example
of a thread being blocked. It can only be unblocked by another thread
calling gsem_V().
If the value of the semaphore is greater than or equal to zero, then the thread does not have to be blocked. However, gsem_P() still cannot return. Instead, its continuation must be called. Rather than call it directly in gsem_P() what happens is that pt_self is put at the beginning of the ready queue and block_myself() is then called. This means that the continuation is indeed called, but not until the stack is reset in block_myself(). Make sure you understand how this works.
gsem_P(g, function, arg)
Gsem g;
void (*function)();
void *arg;
{
Thread *p;
if (Readyq == NULL) pt_initialize();
g->val--;
p = pt_self;
p->function = function;
p->arg = arg;
/* If blocking, put the continuation on the semaphore's queue, otherwise
put the continuation on the front of the ready_queue, and call
block_myself(). The reason for this is to pop off all the stack
frames and start anew */
if (g->val < 0) {
dl_insert_b(g->queue, p);
p->state = BLOCKED;
if (debug) fprintf(stderr, "0x%x: blocking on semaphore 0x%x\n",
pt_self, g);
} else {
dl_insert_a(Readyq, p);
p->state = READY; /* This is not really necessary, since it's going
on the head of the queue */
if (debug) fprintf(stderr, "0x%x: P called but no blocking on 0x%x\n",
pt_self, g);
}
block_myself();
}
gsem_V() is more straightforward. It increments the semaphore's value, and if that is less than or equal to zero, then there is a thread on the queue that needs to be awaken. It does this by removing the first thread off the queue, and putting it onto the ready queue. It then returns to its caller.
gsem_V(g)
Gsem g;
{
Thread *p;
Dlist d;
if (Readyq == NULL) pt_initialize();
g->val++;
/* If g->val <= 0, unblock a thread */
d = g->queue;
if (g->val <= 0) {
d = d->flink;
p = (Thread *) d->val;
dl_delete_node(d);
dl_insert_b(Readyq, p);
p->state = READY;
if (debug) fprintf(stderr, "0x%x: V called on 0x%x -- waking up 0x%x\n",
pt_self, g, p);
} else {
if (debug) fprintf(stderr, "0x%x: V called on 0x%x no one to wake\n",
pt_self, g);
}
}
pt_joinall(function, arg)
void (*function)();
void *arg;
{
if (Readyq == NULL) {
pt_initialize();
}
pt_self->function = function;
pt_self->arg = arg;
pt_self->state = JOINING;
Joinall = pt_self;
block_myself();
}
pt_join() is a little trickier. There are two cases that it must
worry about. The first is if the thread with which it wants to join
(I'll call it the joinee) has
not exited yet. In such a case, the current thread (the joiner)
must block. Thus,
it sets its continuation. It also needs to set itself up so that when the
joinee exits, it can unblocks the joiner. This is done by setting the
joiner field in the joinee's thread struct.
The second case is if the joinee has already exited. In this case, the joinee's state will be set to ZOMBIE. If so, the joinee's thread struct is freed, and the joiner puts itself at the beginning of the ready queue (as in gsem_P() above).
In either case, pt_join() ends by calling block_myself().
pt_join(thread, function, arg)
Thread *thread;
void (*function)();
void *arg;
{
int fnd;
Rb_node r;
if (Readyq == NULL) pt_initialize();
if (thread->joiner != NULL) {
fprintf(stderr, "Called pt_join on a thread twice\n");
exit(1);
}
/* If the thread is a zombie -- free it and go directly to the
continuation */
pt_self->function = function;
pt_self->arg = arg;
if (thread->state == ZOMBIE) {
free(thread);
pt_self->state = READY; /* Unnecessary -- see P() */
dl_insert_a(Readyq, pt_self);
/* Otherwise, block the thread as joining */
} else {
thread->joiner = pt_self;
pt_self->state = JOINING;
}
block_myself();
}
Finally, pt_exit() is called when a thread wants to exit.
It is also called in block_myself() when a continuation returns
because that means that the thread should exit. It performs one of
three actions:
pt_exit()
{
Thread *p;
/* If there is a joiner, put it back on the ready queue and free yourself.
Otherwise, become a zombie */
if (pt_self->joiner != NULL) {
p = pt_self->joiner;
p->state = READY;
dl_insert_b(Readyq, p);
free(pt_self);
block_myself();
} else if (Joinall != NULL) {
free(pt_self);
block_myself();
} else {
pt_self->state = ZOMBIE;
block_myself();
}
}
pt_sleep(sec, function, arg)
int sec;
void (*function)();
void *arg;
{
long t;
Thread *p;
if (Readyq == NULL) pt_initialize();
p = pt_self;
p->function = function;
p->arg = arg;
if (sec <= 0) {
dl_insert_b(Readyq, p);
p->state = READY;
} else {
t = time(0)+sec;
rb_inserti(Sleepq, t, p);
p->state = SLEEPING;
}
block_myself();
}
Now, sleeping threads are awaken in block_myself(). Before it
processes the ready queue, it checks the current time against
the sleep queue, and puts all threads that should be awaken into the
ready queue. The code is below:
block_myself()
{
...
if (!rb_empty(Sleepq)) {
t = time(0);
while(!rb_empty(Sleepq) && rb_first(Sleepq)->k.ikey <= t) {
p = (Thread *) (rb_first(Sleepq)->v.val);
p->state = READY;
dl_insert_b(Readyq, p);
rb_delete_node(rb_first(Sleepq));
}
}
...
static block_myself()
{
Dlist d;
Thread *p;
void (*function)();
void *arg;
long t;
if (Readyq == NULL) pt_initialize();
/* Always longjmp down to pop all thread frames off the stack */
if (first_time) {
first_time = 0;
setjmp(thebuf);
} else {
longjmp(thebuf, 1);
}
/* If the sleep queue is not empty, check to see if any sleepq
elements should come off of the queue */
if (!rb_empty(Sleepq)) {
t = time(0);
while(!rb_empty(Sleepq) && rb_first(Sleepq)->k.ikey <= t) {
p = (Thread *) (rb_first(Sleepq)->v.val);
p->state = READY;
dl_insert_b(Readyq, p);
rb_delete_node(rb_first(Sleepq));
}
}
/* Call the first thread on the ready queue */
if (!dl_empty(Readyq)) {
d = Readyq->flink;
p = (Thread *) d->val;
function = p->function;
arg = p->arg;
dl_delete_node(d);
pt_self = p;
p->state = RUNNING;
(*function)(arg);
/* If the function returns, the thread should exit */
pt_exit();
}
/* Otherwise, if there are sleepers, sleep until one of them is ready */
else if (!rb_empty(Sleepq)) {
t = rb_first(Sleepq)->k.ikey-t;
sleep(t);
block_myself();
}
/* Otherwise, there are no more threads to run. If there is
a joinall continuation, call it. Otherwise, exit */
if (Joinall != NULL) {
p = Joinall;
p->state = READY;
dl_insert_b(Readyq, p);
Joinall = NULL;
block_myself();
}
fprintf(stderr, "No more threads to run\n");
exit(0);
}