Douglas W. Jones
Background A user-level thread package for UNIX might begin with a call to the following routine:
(void) thsetup(int n; (void) *p(); int s) { int i; thheapinit(); thschedinit(); thcreate(p, s); for (i = 1; i < n; i++) { if (fork() != 0) { /* child */ thdispatch(); } } thdispatch(); }This converts the parent process into n parallel processes all running thdispatch9(), a routine that takes ready threads off the thread ready list and runs them. Prior to this, this routine initializes the shared heap (the only memory area shared by the n parallel processes) and initializes the thread ready list before creating a single thread running the function p, with a stack of size s. New threads are created by the following routine:
(void) thcreate((void) *p(); int s);This creates and schedules one thread in the thread ready list. This thread will execute the code in function p, using a stack of size s bytes to execute that code.
The following additional thread scheduling and synchronization operations are in our elementary UNIX-based user-level thread package:
(thsem *) thseminit(); -- create and return a pointer to a thread-semaphore -- in the shared heap (void) thwait(thesem s); -- wait on a thread-semaphore (void) thsignal(thesem s); -- signal on a thread-semaphore (void) thexit(thesem s); -- exit on a thread-semaphore (void *) thmalloc(n); -- allocate n bytes of shared heap (void *) thfree(void * p); -- deallocate a block of shared heap
First, the standard heap manager operates on a heap that is private to each process and does not operate on a shared memory region common to the processes that are acting as compute servers for the thread package.
Second, the shared heap is shared by multiple processes, each possibly running on a different CPU. Therefore, mutual exclusion is required, and no mutual exclusion is included in the standard heap manager.
In addition, note that there is no way to pass a parameter to a thread at the time it is launched. As a result, there is no way to establish communication between threads, except, perhaps, through files. This is pretty miserable!
This implementation omits one detail: After a thread closes a file, the file must be closed separately in each thread support package! To do this, we need some function in the thread system that will search the thread file table for files that are officially closed (at the thread level) but not yet closed in the current thread support process. When such a file is found, it can be moved one step closer to final closure by closing it, at the UNIX level, in that thread support process.
The best time to do this might be at the time a new file is opened. As the open service searches for an available thread file descriptor table entry, it can work on finishing closing any file that is still not all the way closed. If the open service does a thread relinquish each time it fails to find a completely closed file, it will eventually (at random) have a chance to run on every thread support processor, and this will give it a chance to finish closing some file, after some number of tries, thus allowing the open operation to succeede eventually.
On the other hand, this same natural approach to achieving a balanced load can be viewed as a very active and effective load balancing algorithm; every time a process changes state from ready to running, it migrates to a specific CPU, so this environment involves very frequent process migration!