157 lines
5.9 KiB
Plaintext
157 lines
5.9 KiB
Plaintext
|
|
||
|
Threading in Mono
|
||
|
=================
|
||
|
|
||
|
0. Terminology
|
||
|
--------------
|
||
|
|
||
|
"Main thread" - The initial OS-native thread that the
|
||
|
application started with.
|
||
|
|
||
|
"Helper thread" - A native thread created internally
|
||
|
by the runtime, such as the finalizer thread, or an
|
||
|
asynchronous delegate invocation thread. These
|
||
|
threads can run managed code.
|
||
|
|
||
|
"Primary CLR thread" - The native thread that called
|
||
|
the Main() method when executing an assembly.
|
||
|
|
||
|
"Secondary CLR thread" - A native thread created by a
|
||
|
program that instantiates a System.Threading.Thread object
|
||
|
and calls its Start() method.
|
||
|
|
||
|
1. Thread exit behaviour in the standalone mono runtime
|
||
|
-------------------------------------------------------
|
||
|
|
||
|
The correct behaviour of the runtime should be:
|
||
|
|
||
|
a) If Main() returns, the runtime should wait for all
|
||
|
foreground secondary CLR threads to finish. The
|
||
|
wording in the class documentation states: "Once all
|
||
|
foreground threads belonging to a process have
|
||
|
terminated, the common language runtime ends the
|
||
|
process by invoking Abort on any background threads
|
||
|
that are still alive." Testing seems to indicate that
|
||
|
the background thread can't cancel the Abort by
|
||
|
catching the ThreadAbortException and calling
|
||
|
ResetAbort here. Indeed, not even the finally block
|
||
|
seems to be executed.
|
||
|
|
||
|
b) if any of the primary CLR thread, a secondary CLR
|
||
|
thread or a helper thread calls
|
||
|
System.Environment.Exit(), the application should
|
||
|
terminate immediately without waiting for foreground
|
||
|
primary or secondary CLR threads to finish.
|
||
|
|
||
|
c) if the primary CLR thread throws an uncaught
|
||
|
exception, the application should terminate
|
||
|
immediately without waiting for secondary CLR threads
|
||
|
to finish. This might be implemented internally by
|
||
|
pretending that all the running secondary CLR threads
|
||
|
are background threads.
|
||
|
|
||
|
d) if a secondary CLR thread throws an uncaught
|
||
|
exception that thread should terminate and all other
|
||
|
threads should continue to execute.
|
||
|
|
||
|
e) if a helper thread throws an uncaught exception and
|
||
|
that thread happens to be the GC finalizer thread,
|
||
|
testing seems to indicate that the exception stack
|
||
|
trace is displayed as normal, and the exception is
|
||
|
then ignored (as though there is a try {} catch{}
|
||
|
around all finalizers that just prints the stack
|
||
|
trace.) Calling Abort() on the GC finalizer thread
|
||
|
also does not cause it to exit: it behaves as though
|
||
|
the ThreadAbortException is caught and ResetAbort is
|
||
|
called. Asynchronous delegate helper threads should
|
||
|
behave as secondary CLR threads, but uncaught
|
||
|
exceptions should be rethrown on the thread that calls
|
||
|
EndInvoke().
|
||
|
|
||
|
|
||
|
The difficulties happen with cases b) and c):
|
||
|
|
||
|
The current implementation of
|
||
|
System.Environment.Exit() calls exit(2) directly,
|
||
|
which is rather unfriendly: it prevents any runtime
|
||
|
cleanup, statistics gathering, etc. and is pretty
|
||
|
obnoxious to embedded code.
|
||
|
|
||
|
The current exception handling code calls ExitThread()
|
||
|
(emulated with pthread_exit() in the io-layer) if an
|
||
|
exception is not caught.
|
||
|
|
||
|
When called from the main thread, both POSIX
|
||
|
pthread_exit() and w32 ExitThread() block if there are
|
||
|
other threads still running (in the w32 case, if there
|
||
|
are other foreground threads still running; threads
|
||
|
can set as background.) If the main thread is also
|
||
|
the primary CLR thread, then the application will
|
||
|
block until all other threads (including helper
|
||
|
threads) terminate. Some helper threads will not
|
||
|
terminate until specifically told to by the runtime:
|
||
|
for example, the GC finalizer thread needs to run
|
||
|
until all of the primary and secondary CLR threads
|
||
|
have finished.
|
||
|
|
||
|
Also, if the main thread is also the primary CLR
|
||
|
thread, the runtime loses the opportunity to do any
|
||
|
cleaning up. Adding a special case to call exit(2)
|
||
|
instead of ExitThread() in the primary CLR thread
|
||
|
suffers from the same problems as
|
||
|
System.Environment.Exit() calling exit(2).
|
||
|
|
||
|
|
||
|
The simple solution is to run the primary CLR thread
|
||
|
in a new native thread, leaving the main thread free
|
||
|
for housekeeping duties. There still needs to be some
|
||
|
special handling for the case where the primary CLR
|
||
|
thread fails to catch an exception: the secondary CLR
|
||
|
threads then need to be terminated.
|
||
|
|
||
|
When the primary and secondary CLR threads have all
|
||
|
terminated, the helper threads can be killed off and
|
||
|
the runtime can clean itself up and exit.
|
||
|
|
||
|
|
||
|
|
||
|
2. Thread initialisation
|
||
|
------------------------
|
||
|
|
||
|
Threads have to undergo some initialisation before
|
||
|
managed code can be executed. A
|
||
|
System.Threading.Thread object must be created, and
|
||
|
the thread details need to be stored so that the
|
||
|
threads can be managed later. The JIT needs to record
|
||
|
the last managed frame stack pointer in a TLS slot,
|
||
|
and the current Thread object is also recorded.
|
||
|
|
||
|
New threads created by managed calls to
|
||
|
System.Threading.Thread methods will have all needed
|
||
|
initialisation performed. Threads created by the
|
||
|
runtime with calls to mono_thread_create() will too.
|
||
|
Existing threads can be passed to the runtime; these
|
||
|
must call mono_thread_attach() before any CLR code can
|
||
|
be executed on that thread.
|
||
|
|
||
|
|
||
|
3. Constraints on embedding the Mono runtime
|
||
|
--------------------------------------------
|
||
|
|
||
|
The discussion above concerning application behaviour
|
||
|
in the event of threads terminating, whether by
|
||
|
returning from the start function, throwing uncaught
|
||
|
exceptions or by calling System.Environment.Exit(),
|
||
|
only really applies to the standalone Mono runtime.
|
||
|
|
||
|
An embedding application should specify what behaviour
|
||
|
is required when, for example,
|
||
|
System.Environment.Exit() is called. The application
|
||
|
is also responsible for its own thread management, and
|
||
|
it should be prepared for any of the primary CLR
|
||
|
thread or secondary CLR threads to terminate at any
|
||
|
time. The application should also take into account
|
||
|
that the runtime will create helper threads as needed,
|
||
|
as this may cause pthread_exit() or ExitThread() to
|
||
|
block indefinitely, as noted above.
|