开发者

Garbage collection of threads

Do I need to protect Thread objects from the Garbage Collector? What about the object that contains the function that the thread runs?

Consider this simple server:

class Server{
    readonly TcpClient client;

    public Server(TcpClient client){this.client = client;}

    public void Serve(){
        var stream = client.GetStream();
        var writer = new StreamWriter(stream);
        var reader = new StreamReader(stream);

        writer.AutoFlush = true;
        writer.WriteLine("Hello");

        while(true){
            var input = reader.ReadLine();
            if(input.Trim() == "Goodbye")
                break;
            writer.WriteLine(input.ToUpper());
        }

        client.Close();
    }
}
static void Main(string[] args){
    var listener = new TcpListener(IPAddress.Any, int.Parse(args[0]));
    listener.Start();

    while(true){
        var client = listener.AcceptTcpClient();
        var server = new Serve开发者_Python百科r(client);
        var thread = new Thread(server.Serve);
        thread.Start();
    }
}

Should I wrap my thread objects in some sort of static collection to keep them from being swept up by the garbage collector?

Presumably, if the Thread object itself stays alive, then the Server object will live, because the thread holds a reference to the delegate which holds a reference to the target object. Or maybe the thread object itself is collected, but the actual thread continues to run. Now the Server object is up for collection. And then what happens if it tries to access its field(s)?

Garbage collection makes my head spin some times. I'm glad I usually don't have to think about it.

Given the potential gotchas here, I would like to believe that the garbage collector is smart enough not to collect thread objects when the thread itself is still executing, but I can't find any documentation saying so. Reflector is of little help here, since much of the Thread class is, unsurprisingly, implemented in MethodImplOptions.InternalCall functions. And I'd rather not dig through my old outdated copy of SSCLI for answers (both because it's a pain, and because it's not a sure answer).


It is quite simple. The real execution thread is not Thread object. The program is executing in real Windows threads which stay alive regardless of what your .NET garbage collector does with your Thread objects. So it is safe for you; you don't need to care about Thread objects if you just want the program to keep running.

Also note that your threads don't get collected when they run, because they in fact belong to the application "roots". (Roots - it's how garbage collector knows what is alive.)

More details: A managed Thread object is accessible via Thread.CurrentThread - that is something like a global static variable and those don't get collected. As I wrote earlier: Any managed thread which was started and now executing any code (even outside of .NET), doesn't lose its Thread object, because it is firmly connected to "roots".


As long as your thread is executing, it will not be collected.


The start parameter (which is server.Serve in your case) in the thread constructor is a delegate which you knew already.

Presumably, if the Thread object itself stays alive, then the Server object will live, because the thread holds a reference to the delegate which holds a reference to the target object

This is what C# in Depth by Jon Skeet has to say about the life of the target of a delegate

It’s worth being aware that a delegate instance will prevent its target from being garbage collected, if the delegate instance itself can’t be collected.

So yes server won't be collected as long as the var thread is in scope. Hoever if var thread goes out of scope before thread.start is called (and there's no other references) then yes it can be collected.

Now the big question. Thread.Start() is called and the thread has gone out of scope before server.Serve has completed can the GC collect the thread.

Rather than dig around lets just test it

class Program
{
    static void Main(string[] args)
    {
        test();
        GC.Collect(2);
        GC.WaitForPendingFinalizers();
        Console.WriteLine("test is over");
    }

    static void test()
    {
        var thread = new Thread(() =>  {
            long i = 0;

            while (true)
            {
                i++;   
                Console.WriteLine("test {0} {1} {2} ", Thread.CurrentThread.Name, Thread.CurrentThread.ManagedThreadId i);
                Thread.Sleep(1000); //this is a demo so its okay
            }
        });
        thread.Name = "MyThread";
        thread.Start();

    }

}

This is the output. (after adding the threadID)

test MyThread 3 1
test is over
test MyThread 3 2
test MyThread 3 3
test MyThread 3 4
test MyThread 3 5

So I call a method that creates a thread and starts it. The method ends so the var thread is out of scope. But even though I've induced a GC and the thread variable is out of scope the thread keeps running.

So as long as you Start the thread before it goes out of scope everything will work as expected

Update To clarify GC.Collect

Forces an immediate garbage collection of all generations.

Anything that can be collected will be. This is not as the comments suggests "little more than an expression of intent". (If it were there would be no reason for the caution against calling it) However I added the argument "2" to make sure it was gen0 through gen2.

Your point about the finalizers was worth noting however. So I added GC.WaitForPendingFinalizers which

... suspends the current thread until the thread that is processing the queue of finalizers has emptied that queue.

This means that any finalizers that needed to be processed have indeed been processed.

The point of the sample was that as long as you start the thread it will run until its aborted or finished and that the GC won't somehow abort the thread just because the var thread has gone out of scope.

As an aside Al Kepp is correct that indeed once you start the thread the System.Threading.Thread is rooted. You can find this out by using the SOS extension.

e.g.

!do 0113bf40
Name:        System.Threading.Thread
MethodTable: 79b9ffcc
EEClass:     798d8ed8
Size:        48(0x30) bytes
File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
79b88a28  4000720        4 ....Contexts.Context  0 instance aaef947d00000000 m_Context
79b9b468  4000721        8 ....ExecutionContext  0 instance aaef947d00000000 m_ExecutionContext
79b9f9ac  4000722        c        System.String  0 instance 0113bf00 m_Name
79b9fe80  4000723       10      System.Delegate  0 instance 0113bf84 m_Delegate
79ba63a4  4000724       14 ...ation.CultureInfo  0 instance aaef947d00000000 m_CurrentCulture
79ba63a4  4000725       18 ...ation.CultureInfo  0 instance aaef947d00000000 m_CurrentUICulture
79b9f5e8  4000726       1c        System.Object  0 instance aaef947d00000000 m_ThreadStartArg
79b9aa2c  4000727       20        System.IntPtr  1 instance 001D9238 DONT_USE_InternalThread
79ba2978  4000728       24         System.Int32  1 instance        2 m_Priority
79ba2978  4000729       28         System.Int32  1 instance        3 m_ManagedThreadId
79b8b71c  400072a      18c ...LocalDataStoreMgr  0   shared   static s_LocalDataStoreMgr
    >> Domain:Value  0017fd80:NotInit  <<
79b8e2d8  400072b        c ...alDataStoreHolder  0   shared TLstatic s_LocalDataStore

Is this the thread with the name MyThread

!do -nofields 0113bf00
Name:        System.String
MethodTable: 79b9f9ac
EEClass:     798d8bb0
Size:        30(0x1e) bytes
File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String:      MyThread

Why yes, it is.

What's it rooted to?

!GCRoot 0113bf40
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 7708 OSTHread 1e1c
Scan Thread 4572 OSTHread 11dc
Scan Thread 9876 OSTHread 2694
ESP:101f7ec:Root:  0113bf40(System.Threading.Thread)
ESP:101f7f4:Root:  0113bf40(System.Threading.Thread)
DOMAIN(0017FD80):HANDLE(Strong):9b11cc:Root:  0113bf40(System.Threading.Thread)

As the Production Debugging for .NET Framework Applications says it is the root.

Note that !GCRoot was run after it was started. Before it was started it didn't have the HANDLE(Strong).

If the output contains HANDLE(Strong), a strong reference was found. This means that the object is rooted and cannot be garbage collected. Other reference types can be found in the Appendix.

For more info on how managed threads map to OS Threads (and how you can confirm it using SOS extensions) see this article by Yun Jin.


Despite my caution to Conrad Frix about the difficulty in proving things with test code when it comes to either multithreading or GC, let alone both, I myself threw together a simple little test. I think the results put a satisfactory rest to this question.

The more I read the answers posted here, and the more I thought about it, the more it seemed to boil down to one specific question:

How much havoc will the Thread object's finalizer wreak when and if it runs before my thread has exited.

Obviously, the GC itself wouldn't (directly) abort the actual execution thread. And as long as the Server object's Serve() method was executing, its this pointer should be protected as being an object locally accessible to the currently running method.

But if the GC collected and finalized the Thread object, what would happen? Since the thread object is supposed to represent the execution thread, would its finalizer kill the execution thread, bringing them to equilibrium? Or perhaps worse, would it destroy some internal state that the runtime uses to manage the thread?

I really didn't want to pry open Rotor to answer these questions, both because it's obtuse, and because I didn't want to rely on the SSCLI implementation; not for this.

But then I started wondering if I wasn't thinking about this somewhat backwards. I was thinking of the Thread object as controlling the execution thread, implying that the latter would live and die with the former.

But in fact, the Thread class is merely an interface to the execution thread—a convenient abstraction. It, instead, must live and die with the execution thread, not the other way around.

Looking at it from this perspective, I thought the framework must already do the job of keeping the abstraction alive, right? Of course! I'd used it a million times. It's the Thread.CurrentThread property!

The simplest of tests revealed that the created Thread object was indeed accessible from within the framework itself:

Thread t;
t = new Thread(o=>Console.WriteLine(o == Thread.CurrentThread));
t.Start(t);

This tiny test confirmed that, no matter whether I keep a reference to my Thread objects, the framework does. And with that known, everything else is a given. The Server object lives and the execution thread keeps on executing.

Yay! I can go back to not having to think about the garbage collector for a while!

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜