NSPR’s position on abrupt thread termination¶
This memo describes my position on a facility that is currently under discussion for inclusion in the NetScape Portable Runtime (NSPR); the ability of a thread to abruptly exit. I resist including this function in NSPR because it results in bad programming practice and unsupportable programs.
Threads are not processes.
Abrupt termination has been available in the UNIX/C environment for some
time (exit()
), and I assume that the basic semantics defined there
are applicable here. In that environment, exit()
may be called and
any time, and results in the calling thread’s immediate termination. In
the situation where it was defined (UNIX), which has only a single
thread of execution, that is equivalent to terminating the process. The
process abstraction is then responsible for closing all open files and
reclaiming all storage that may have been allocated during the process’
lifetime.
This practice does not extend to threads. Threads run within the
confines of a process (or similar abstractions in other environments).
Threads are lightweight because they do not maintain the full protection
domain provided by a process. So in a threaded environment, what is the
parallel to UNIX’ exit()
?
NSPR has defined a function, callable by any thread within a process at
any time, called PR_ProcessExit()
. This is identical to UNIX
exit()
and was so named in an effort to make the obvious even more
so. When called, the process exits, closing files and reclaiming the
process’ storage.
Certain people have been disappointed when NSPR did not provide a functional equivalent to exit just a particular thread. Apparently they have failed to consider the ramifications. If a thread was to abruptly terminate, there is no recording of what resources it owns and should therefore be reclaimed. Those resources are in fact, owned by the process and shared by all the threads within the process.
In the general course of events when programming with threads, it is very advantageous for a thread to have resources that it and only it knows about. In the natural course of events, these resources will be allocated by a thread, used for some period of time, and then freed as the stack unwinds. In these cases, the presence of the data is recorded only on the stack, known only to the single thread (normally referred to as encapsulated).
The problem with abrupt termination is that it can happen at any time, to a thread that is coded correctly to handle both normal and exceptional situations, but will be unable to do so since it will be denied the opportunity to complete execution. It can happen because it called out of its own scope into some lazily implemented library.
NSPR’s answer to this is that there is no abrupt thread termination. All threads must unwind and return from their root function. If they cannot, because of some state corruption, then they must assume that the corruption, like the state, is shared, and their only resource is for the process to terminate.
To make this solution work requires that a function that encounters an error be designed such that it first repairs its immediate state, and then reports that error to its caller. If the caller cannot deal with the failure, it must do the same. This process continues until the thread either recovers from the malady or returns from the root function. This is not all that difficult (though having done it a number of times to already existing code, I will admit it isn’t much fun either).
The implementation of either strategy within the NSPR runtime is not
difficult. That is not what this memo is about. This is about providing
an API that coaxes people to do the right thing in as many ways as
possible. The existence of exit()
in the UNIX/C environment is a
perfect example of how programmers will employ the most expediant
solution available. The definition of the language C is such that
returning from main()
is a perfectly fine thing to do. But what
percentage of C programs actually bother? In UNIX, with its complex
definition of a protection domain, it happens to work (one might even
say it’s more efficient) to exit from anywhere. But threads are not
processes. If threads have to maintain the same type of resource
knowledge as a process, they loose all of their benefit.
Threads are an implementation strategy to provide the illusion of
concurrency within a process. They are alternatives to large state
machines with mostly non-blocking library functions. When the latter is
used to provide concurrency, calling exit()
will terminate the
entire process. Why would anyone expect a thread to behave differently?
Threads are not processes.