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.