线程的垃圾回收

7

我需要保护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对象需要进行垃圾收集。那么如果它尝试访问它的字段会发生什么?

有时候垃圾收集让我感到头晕。我很高兴通常不必考虑它。

鉴于这里潜在的问题,我希望相信当线程本身仍在执行时,垃圾收集器足够聪明,不会收集线程对象,但我找不到任何说明。反编译工具在这里没有多少帮助,因为大部分Thread类是使用MethodImplOptions.InternalCall函数实现的,这并不奇怪。而且我不想通过查看我旧的过时的SSCLI副本来获得答案(因为这很麻烦,而且不是确定的答案)。

4个回答

6
很简单。真正的执行线程不是Thread对象。程序正在实际的Windows线程中执行,这些线程会一直保持活动状态,无论您的.NET垃圾收集器如何处理Thread对象。所以对你来说很安全;如果你只想让程序保持运行,就不需要关心Thread对象。
还要注意,当您的线程运行时,它们不会被回收,因为它们实际上属于应用程序的“根”。 (根-这是垃圾收集器知道什么是活动的方式。)
更多细节:通过Thread.CurrentThread可以访问托管的Thread对象,它类似于全局静态变量,这些变量不会被回收。就像我之前写的那样:任何已启动并正在执行任何代码(甚至在.NET之外)的托管线程都不会失去其Thread对象,因为它与“根”牢固地连接在一起。

@Al 可能是,也可能不是。你能证明这就是原因吗?我可以看到几种替代解释,确实围绕着“聪明”展开,但我宁愿不做猜测。 - Sander
@Sander:很抱歉,我不明白你想证明什么。从应用程序根目录可访问的对象是活动的。当它在托管代码中时,当前上下文和所有线程调用堆栈中存在的所有上下文都属于其中。在我们的情况下,这还包括对Server对象的'this'指针,因此它不能被处理,也不需要其他特殊的智能。我写得含糊不清,请在.NET平台的某本书中阅读确切的定义。 - Al Kepp
当谈到根时,我认为你有所发现。根据http://www.simple-talk.com/dotnet/.net-framework/understanding-garbage-collection-in-.net/,*当前正在运行的方法中的局部变量被视为GC根*。这意味着`this`引用(在我的示例中是`Server`对象)将被视为根,因此不会被回收。`Thread`对象本身并没有受到此概念的保护,但只要它的终结器不中止正在运行的线程(那将是愚蠢的,不是吗?),那么就没有什么可担心的了,对吧? - P Daddy
@P.Daddy:Thread对象可以通过Thread.CurrentThread访问 - 这类似于全局静态变量,不会被回收。正如我之前所写的:任何已启动并正在执行任何代码(甚至在 .net 之外)的托管线程都不会失去其Thread对象,因为它与“根”牢固连接。 - Al Kepp
我接受你的答案主要是因为那个评论。这是我得出的相同结论,但你先说了,而且接受自己的答案有点俗气。作为对未来可能会遇到这个问题的其他人的礼貌,你介意把你的评论内容放到答案正文中吗? - P Daddy
显示剩余3条评论

2
只要您的线程正在执行,它就不会被回收。

1
当然,这是我想要实现的公理,但除了直觉和希望之外,我没有任何东西可以告诉我这一点。您知道有关此事的任何文档吗? - P Daddy

2

在您的情况下,线程构造函数中的start参数(即server.Serve)是一个委托,您已经知道了。

假设Thread对象本身保持活动状态,则Server对象将保持活动状态,因为线程持有对委托的引用,该委托持有对目标对象的引用

这是Jon Skeet的C#深入研究中关于委托目标生命周期的内容

值得注意的是,如果委托实例本身无法被收集,委托实例将防止其目标被垃圾回收。

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

现在的问题是,当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();

    }

}

这是输出。(添加线程ID后)
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扩展程序找出这一点。
!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)

.NET Framework应用程序的生产调试所述,它是根源。

请注意,在启动之前运行了!GCRoot。在启动之前,它没有HANDLE(Strong)。

如果输出包含HANDLE(Strong),则找到了强引用。这意味着对象已经根据,并且无法进行垃圾回收。其他引用类型可以在附录中找到。

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


我一直跟着你,直到你提到测试代码。恐怕你的测试代码并不能证明比我的示例代码更多的东西。任何涉及多线程或GC的事情都存在一定程度的不确定性,这使得很难证明任何东西。你的代码和我的代码都没有对内存施加太大的压力,而且调用GC.Collect()只是表达意图的一种方式,特别是在.NET 3.5及以下版本中。更重要的是,Thread类有一个终结器,这会使这些对象的回收变得复杂(并延迟)。 - P Daddy
@P Daddy,我已经澄清了我的回答。 - Conrad Frix

1
尽管我曾警告Conrad Frix,在涉及多线程或GC(垃圾回收)时,用测试代码来证明事情的难度很大,更不用说两者都有了,但我自己还是拼凑出了一个简单的小测试。我认为结果对这个问题做出了令人满意的解答。
我越读这里发布的答案,越思考它,似乎归结为一个具体的问题:

如果我的线程尚未退出,Thread对象的终结器会造成多大的破坏。

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

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

我真的不想打开Rotor来回答这些问题,因为它很难懂,而且我不想依赖于SSCLI实现;至少对于这个问题不想。

但是后来我开始想,我是否在有些方面反向思考了。我认为Thread对象是控制执行线程的,这意味着后者将与前者一起生存和死亡。

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

从这个角度来看,我认为框架必须已经完成了保持抽象活动的工作,对吧?当然!我已经使用了无数次。这

最简单的测试表明,创建的Thread对象确实可以从框架内部访问:
Thread t;
t = new Thread(o=>Console.WriteLine(o == Thread.CurrentThread));
t.Start(t);

这个小测试证实了无论我是否保留对我的Thread对象的引用,框架都会保留。有了这个认识,其他一切都是确定的。 Server对象存在并且执行线程继续执行。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接