垃圾收集线程

时间:2011-01-18 06:08:21

标签: .net multithreading

我是否需要保护垃圾收集器中的Thread个对象?包含线程运行的函数的对象怎么样?

考虑这个简单的服务器:

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 Server(client);
        var thread = new Thread(server.Serve);
        thread.Start();
    }
}

我应该将我的线程对象包装在某种静态集合中,以防止它们被垃圾收集器扫除吗?

据推测,如果Thread对象本身保持活动状态,则Server对象将存活,因为该线程持有对委托的引用,该委托持有对目标对象的引用。或者可能收集线程对象本身,但实际线程继续运行。现在Server对象即可进行收集。如果它试图访问其字段会发生什么?

垃圾收集使我的头部旋转了一些时间。我很高兴我通常不必考虑它。

考虑到潜在的问题,我想相信垃圾收集器足够智能,当线程本身仍在执行时不能收集线程对象,但我找不到任何文档说明。 Reflector在这方面没有多大帮助,因为Thread类的大部分内容都在MethodImplOptions.InternalCall函数中实现。而且我宁愿不通过我过时的旧SSCLI副本来寻找答案(因为这是一个痛苦,因为它不是一个肯定的答案)。

4 个答案:

答案 0 :(得分:6)

这很简单。真正的执行线程不是Thread对象。该程序在真正的Windows线程中执行,无论.NET垃圾收集器对您的Thread对象执行什么操作,它都会保持活动状态。所以对你来说是安全的;如果你只是希望程序继续运行,你不需要关心Thread对象。

另请注意,您的线程在运行时不会被收集,因为它们实际上属于应用程序“根”。 (根源 - 垃圾收集者知道什么是活着的。)

更多详细信息:托管的Thread对象可以通过Thread.CurrentThread访问 - 这类似于全局静态变量,并且不会收集这些变量。正如我之前写的:任何已启动并且现在正在执行任何代码(甚至在.NET之外)的托管线程都不会丢失其Thread对象,因为它与“根”紧密相连。

答案 1 :(得分:2)

只要你的线程正在执行,它就不会被收集。

答案 2 :(得分:1)

thread constructor中的start参数(在您的情况下是server.Serve)是您已经知道的委托。

  推测,如果是Thread对象   本身保持活着,然后是服务器   对象会活,因为线程   持有对代表的引用   它包含对目标的引用   对象

这就是Jon Skeet在深度中的C#对代表目标的生命所说的话

  

值得注意的是一位代表   实例将阻止其目标   垃圾收集,如果   委托实例本身不可能   集。

只要server在范围内,就不会收集var thread。但是,如果var thread在调用thread.start之前超出范围(并且没有其他引用),那么可以收集它。

现在是个大问题。调用Thread.Start()并且在server.Serve完成之前线程已超出范围,GC可以收集线程。

而不是四处挖掘,只是测试它

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();

    }

}

这是输出。 (在添加threadID之后)

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

所以我调用一个创建线程并启动它的方法。该方法结束,因此var thread超出范围。但即使我已经引发了GC并且线程变量超出了范围,线程也会继续运行。

因此,只要您在超出范围之前启动线程,一切都将按预期工作

<强>更新 澄清GC.Collect

  

强制所有世代立即进行垃圾收集。

任何可以收集的东西都是。这不是评论所暗示的“仅仅是意图的表达”。 (如果没有理由不提出反对的话)但是我添加了参数“2”以确保它是gen0到gen2。

然而,关于终结者的观点值得注意。所以我添加了GC.WaitForPendingFinalizers

  

...挂起当前线程,直到处理终结器队列的线程清空该队列。

这意味着任何需要处理的终结器确实已被处理。

示例的重点是,只要你启动线程,它就会一直运行直到它被中止或完成,并且GC不会因为var thread超出范围而以某种方式中止线程。

暂且放弃 Al Kepp是正确的,确实一旦启动线程,System.Threading.Thread就会生根。您可以使用SOS扩展程序找到它。

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

这是名为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

为什么是,它是。

根源是什么?

!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)

正如Production Debugging for .NET Framework Applications所说它是根。

请注意!GCRoot在启动后运行。在它开始之前它没有HANDLE(强)。

  

如果输出包含HANDLE(强),   找到了一个强有力的参考。这个   意味着对象是root的   不能垃圾收集。其他   引用类型可以在   附录

有关托管线程如何映射到操作系统线程的更多信息(以及如何使用SOS扩展确认它),请参阅Yun Jin的this article

答案 3 :(得分:1)

尽管我的caution to Conrad Frix关于使用多线程或GC进行测试代码验证的难度,但更不用说两者了,我自己也只是进行了一个简单的小测试。我认为结果令人满意地接受了这个问题。

我越是阅读这里发布的答案,我想的越多,它就越能归结为一个特定的问题:

  

Thread对象的终结器在我的线程退出之前运行时会发生多大的破坏。

显然,GC本身不会(直接)中止实际的执行线程。只要Server对象的Serve()方法正在执行,它的this指针就应该被保护为当前正在运行的方法可以访问的对象。

但是如果GC收集并最终确定 Thread对象,会发生什么?由于线程对象应该代表执行线程,它的终结器会杀死执行线程,使它们达到平衡吗?或者更糟糕的是,它会破坏运行时用来管理线程的一些内部状态吗?

真的不想撬开Rotor回答这些问题,因为它是钝的,而且因为我不想依赖SSCLI实现;不是为了这个。

但后来我开始怀疑自己是不是在考虑这种情况。我认为Thread对象是控制执行线程,暗示后者会与前者生存并死亡。

但事实上,Thread类只是执行线程的接口 - 一个方便的抽象。相反,它必须与执行线程一起生存和死亡,而不是相反。

从这个角度来看,我认为框架必须已经完成了保持抽象的工作,对吧?当然!我用了一百万次。这是Thread.CurrentThread属性!

最简单的测试表明,创建的Thread对象确实可以从框架内部访问:

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

这个微小的测试证实,无论是否保留对Thread个对象的引用,框架都会这样做。在知道的情况下,其他一切都是给定的。 Server对象存在,执行线程继续执行。

耶!我可以回去不用考虑垃圾收集器一段时间了!