I've noticed this type of behavior before, and it occurred to me to ask a question this time:
I have a simple "proof of concept" program that spawns a few threads, waits for them to do some work, then exits.
But Main
isn't returning unless I call server.Close()
(which closes the socket and ends the server's thread's execution):
private void Run()
{
var server = StartServer(); //Starts a thread in charge of listening on a socket
_serverResetEvent.WaitOne();
ThriftProtocolAccessManager protocolManager = CreateProtocolManager(); //Doesn't create any threads
const int numTestThreads = 10;
Barrier testCompletedBarrier;
Thread[] testThreads = GenerateTestThreads(numTestThreads, protocolManager, out testCompletedBarrier); //Creates x threads, where x is numTestThreads
testThreads
.AsParallel()
.ForAll(thread => thread.Start()); //Start them "at the same time" (For testing purposes
testCompletedBarrier.SignalAndWait(); //Barrier has participants equal to numTestThreads + 1 and each thread calls this
//server.Close() would go here. When it is here, the program returns as expected
Console.WriteLine("All Threads Complete"); //This is getting called
}
private static void Main(string[] args)
{
new Program().Run();
Console.WriteLine("Run completed"); //This is also called
}//The debugger confirms this brace is reached as well
According to article 10.2, "Application termination" of the ECMA C# language specs:
If the return type of the entry point method is
void
, reaching the right brace (}
) which terminates that method, or executing areturn
statement that has no expression, results in a termination status code of 0.
The debugger confirms that the right brace is being reached, but the standard doesn't explicitly say that leaving Main
will exit the application, only that the termination status code is set.
It also mentions that:
...finalizers for all of [the application's] objects that have not yet been garbage collected are called, unless such cleanup has been suppressed (by a call to the library method
GC.SuppressFinalize
, for example).
I suspected that behind the scenes a finalizer might be the problem, since the server object implements IDisposable
, and it's not uncommon to have a finalizer that calls Dispose
. But the CLR limits finalizers to two seconds of execution when the program is being terminated (Just in-case something strange was happening with the timeout I tried calling GC.SuppressFinalize
on the server object and got the same result).
I'm a bit stumped as to what the server thread could be doing to hold up termination of the application indefinitely.
The wording used in the @Carsten König's link lead me to realize I was looking in the wrong documentation. The problem was indeed that the thread that was starting the server implementation was a foreground thread, changing it to a background thread causes the ThreadPool implementation to behave as expected.
It seems that it the ECMA standard defers the specific termination behavior (The CLI documentation didn't mention anything about it either). I'm still looking to see if I can find a general document that describes the entire termination procedure in greater detail.