D. J. Bernstein

The npthread library interface

The npthread library manages a collection of threads: pointers to functions that should be called when specified conditions occur.

For example, you can tell npthread to call the handle0() function whenever data is available to be read on descriptor 0, and to call the handle6() function whenever data is available to be read on descriptor 6. If neither descriptor has data, npthread will wait until data arrives.

The npthread library allows the following seven types of threads:

Preemptive threads versus non-preemptive threads

Some thread libraries provide preemptive threads: a function can be called even while another function is active.

The npthread library provides non-preemptive threads: specifically, a function must return before npthread calls another function. For example, say you've told npthread to call the handle0() function whenever data is available to be read on descriptor 0, and to call the handle6() function whenever data is available to be read on descriptor 6. If descriptors 0 and 6 both have data, npthread will call handle0() and then handle6(), or it might instead call handle6() and then handle0(); but it will not overlap handle0() and handle6().

Other thread libraries provide semi-preemptive threads: a function does not need to return before another function is called, but it does need to take some explicit action, such as calling a yield() function. (Semi-preemptive threads are sometimes confusingly called non-preemptive threads. The word coroutines can refer to either semi-preemptive threads or non-preemptive threads.)

Basic issues to consider in deciding whether to use a preemptive thread library, a semi-preemptive thread library, or a non-preemptive thread library:

Separate UNIX processes are always preemptive and will take advantage of separate CPUs. A common approach is to split tasks across several UNIX processes, with non-preemptive threads inside each process.

The npthread library replaces my old sigsched library. Other non-preemptive thread libraries: Niels Provos's libevent, Dan Egnor's liboop, libwww's HTEvent, and GLib's Main Event Loop. Some semi-preemptive thread libraries: Keld Helsgaun's COROUTINE, Ralf Engelschall's Pth (formerly NPS), and Netscape's State Threads. The most common preemptive-thread interface is the POSIX thread (Pthread) interface supported by various libraries.


Starting threads

     #include "npthread.h"
     int (*f)(int64);
     int64 n;
     npthread_add(f,n);
npthread_add creates a new ``important'' thread that will call f(n) as soon as possible (after npthread_start is called). It returns 1 to indicate success.

If npthread_add runs out of memory, it returns 0 without changing the list of threads.


     #include "npthread.h"
     int (*f)(int64);
     int64 n;
     int s;
     npthread_addsignal(f,n,s);
npthread_addsignal creates a new ``unimportant'' thread that will call f(n) when UNIX signal s is received (after npthread_start is called). It returns 1 to indicate success.

If npthread_addsignal runs out of memory, it returns 0 without changing the list of threads.

UNIX signals handled by npthread are not an exception to the rule that one thread does not preempt another. For example, after

     npthread_add(compute,0);
     npthread_addsignal(cleanup,0,SIGTERM);
     npthread_start();
the function compute(0) will be called. If a SIGTERM signal arrives while compute(0) is running, it has no immediate effect; cleanup(0) will be called after compute(0) returns.

Do not attempt to use UNIX signals as counters. Two occurrences of a signal in quick succession will be combined into a single occurrence of the signal; a thread watching the signal will be called once, not twice.


     #include "npthread.h"
     npthread_start(void);
npthread_start looks for a thread whose call condition is satisfied, calls the thread function, and repeats, as long as there is at least one ``important'' thread left. It then returns 1.

A thread function is required to return one of the following numbers:

If npthread_start has trouble preparing internal resources, it returns 0, setting errno to indicate the error. A failure of this type cannot happen once npthread_start begins running threads.

npthread_start does not check every call condition every time it calls a thread function. Its main loop checks many call conditions and then calls many threads. Specifically:

  1. Top of the loop: npthread_start checks whether any ``important'' threads are left. It returns if not.
  2. npthread_start checks which descriptors are readable and writable. It marks as ``ready'' every thread waiting for those descriptors.
  3. npthread_start checks the current time. It marks as ``ready'' every thread to be called before this time. It also marks as ``ready'' every thread to be called as soon as possible.
  4. npthread_start checks which UNIX signals have been received. It marks as ``ready'' every thread waiting for those signals.
  5. npthread_start checks for newly exited children. It marks as ``ready'' every thread waiting for those children.
  6. npthread_start marks as ``run now'' each of the ``ready'' threads, and removes all the ``ready'' marks.
  7. For each of the ``run now'' threads, in some order, npthread_start calls the thread function. User-defined flags may be waved at this time; whenever a user-defined flag is waved, every thread waiting for that flag is marked as ``ready.''
  8. npthread_start goes back to the top of the loop.
Do not assume that UNIX signals, flag waves, readable descriptors, etc. take effect immediately. For example, if thread 1 waves a flag that thread 2 is watching, it is possible for thread 1 to be called again before thread 2 is called.

Modifying threads

The following functions modify the current thread. These functions cannot fail; all necessary storage is preallocated by npthread_add.

These functions are for use solely in thread functions. They must not be called outside npthread_start.


     #include "npthread.h"
     int (*f)(int64);
     int64 n;
     npthread_jump(f,n);
npthread_jump tells the npthread library that f(n) is the function to be called by the current thread. npthread_jump is a jump, not a subroutine call: it wipes out the previous function pointer.
     #include "npthread.h"
     npthread_unimportant(void);
     npthread_important(void);
npthread_unimportant changes the current thread from ``important'' to ``unimportant.'' If the thread is already ``unimportant,'' npthread_unimportant leaves it alone.

npthread_important makes the opposite change.


     #include "npthread.h"
     tai6464 t;
     npthread_sleepuntil(t);
npthread_sleepuntil tells the npthread library to call the current thread when the current time has passed t. Any previous call conditions for the current thread are wiped out.
     #include "npthread.h"
     int64 p;
     int status;
     npthread_child(p);
     status = npthread_childstatus();
npthread_child tells the npthread library to call the current thread if child process p has just exited. Any previous call conditions for the current thread are wiped out.

Once that call happens, npthread_childstatus() returns the child's exit status, in the following form:


     #include "npthread.h"
     int64 p;
     npthread_watchflag(p);
     npthread_waveflag(p);
     p = npthread_newflag();
npthread_watchflag tells the npthread library to call the current thread if user-defined flag number p has just been waved. Any previous call conditions for the current thread are wiped out.

npthread_waveflag waves user-defined flag number p.

npthread_newflag returns a new flag number each time it is called: 1, 2, 3, etc. Libraries should use npthread_newflag to obtain their flag numbers so that independent libraries do not bump into each other.

Do not attempt to use user-defined flag waves as counters. If a thread is watching a flag, and the flag is waved twice in quick succession before the thread has a chance to wake up, the thread will be called once, not twice.


     #include "npthread.h"
     int64 d;
     npthread_read(d);
     npthread_write(d);
npthread_read tells the npthread library to call the current thread when file descriptor d is readable. npthread_write tells the npthread library to call the current thread when file descriptor d is writable.

The descriptor must already be open and known to the io library; you must use io_fd() to register descriptors not obtained from io_pipe() etc. The descriptor must remain open until the call condition is changed.

Any previous call conditions for the current thread are wiped out. For example, the npthread_sleepnutil in

     npthread_sleepuntil(tai6464_add(tai6464_now(),networktimeout));
     npthread_read(6);
     return 1;
has no effect. If descriptor 6 is not readable, the thread will not be called again, even after networktimeout expires. In contrast,
     io_timeout(6,tai6464_add(tai6464_now(),networktimeout));
     npthread_read(6);
     return 1;
will impose a time limit on the readability of descriptor 6.

Do not assume that data can actually be read or written merely because of a previous npthread_read or npthread_write. Data that is readable when npthread_start decides to call this thread might be read a moment later by another process. Buffer space available for writing when npthread_start decides to call this thread might be used a moment later by another process.

Do not assume that data has ever been readable or writable merely because of a previous npthread_read or npthread_write. The low-level UNIX routines used by io_wait could have failed to allocate memory; in this case, npthread_start has no choice but to call all threads waiting for descriptors.


     #include "npthread.h"
     npthread_asap();
npthread_asap tells the npthread library to call the current thread whenever possible. This is how the thread starts out after npthread_add.