65.9K
CodeProject 正在变化。 阅读更多。
Home

要不要保留 –关于线程引用的故事!!!

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2011 年 3 月 31 日

CPOL

6分钟阅读

viewsIcon

5693

要不要保留 –关于线程引用的故事!!!

void SomeMethod(int x, double y) {
  // some code
  ....
  new Thread(ThreadFunc).Start();
}

您认为上面的代码怎么样?

有些人可能会说代码看起来没什么问题。有些人可能会说信息不足,无法评论。少数人可能会说像这样启动一个线程(方法中的最后一行)是非常糟糕的,并且该线程有可能在执行的意外点被垃圾回收。这是一个有趣的话题。

在提出我的观点和支持事实之前,简短的答案是:不可以。像这样启动的线程不会像我们预期的那样被垃圾回收,尽管写出这样的代码的人在道德上是疯狂的。好了,我们来讨论一下。

Thread 类与 BCL 中的任何其他引用类型一样。当一个引用类型的实例不再有任何未完成的引用时,它就有资格被垃圾回收。更糟糕的是,即使在执行实例方法时,如果没有对该实例的未完成引用,它也可能成为一个候选者。如果考虑到这些事实,那么在上述代码中随意创建的线程就有可能在执行相关的 ThreadFunc 时随时被回收。让我们用一个简单的示例应用程序来尝试一下。

static void Main(string[] args) 
{
   Console.WriteLine("The Beginning...."); 

   // ThreadFunc inlined as anonymous delegate
   new Thread(delegate() 
   {
      for (int i = 0; i < 1000; i++) 
      {       
         var obj = new Junk(i, i, i.ToString()); 
         Console.WriteLine("{0}, ", i); 
         Thread.Sleep(100); 

         if (i % 10 == 0) 
         {
            GC.Collect(); 
            GC.WaitForPendingFinalizers();
         }
       
      }    
   }).Start(); 

   GC.Collect(); 
   GC.WaitForPendingFinalizers(); 

   Console.WriteLine("At the end!");
}

在上面的示例应用程序中,我在两个重要的执行点强制进行了垃圾回收,并等待了挂起的终结器。

  1. 在主线程结束时
  2. 在线程委托执行过程中

我通过创建一些 Junk 对象来确保 GC 有足够的触发机会。这些都是 GC 触发的一些重要因素。

如果您运行该应用程序,您会发现应用程序不会退出,直到循环完成;尽管主线程运行完毕 — 打印“At the end!”。现在,我们不要争论线程是前台线程,应用程序在所有前台线程执行完毕之前不会退出。是的,如果上述代码中的线程是后台线程,应用程序会在循环完成之前退出。但是,那样的话,我们就创建了一个引用来将其设置为后台线程,然后我们必须停止讨论,因为我们讨论的是在没有持有引用的情况下创建/启动的线程。此外,问题的重点不是应用程序退出,而是应用程序运行。

好的,让我换一种方式来表达,这与我们的上下文相关 — 如果您运行应用程序,您会发现线程函数成功运行到完成(循环 1000 次,创建 1000 个对象,每次循环迭代等待 100 毫秒,在执行过程中触发垃圾回收/等待挂起的终结器)。如果线程被垃圾回收了,它就不会成功运行 1000 次迭代。那么,这是否意味着 CLR 对 Thread 类型有点偏爱?似乎是这样。

如果您使用 Reflector 深入研究 Thread 类型,您会发现它既没有实现 IDisposable,也没有实现终结器。一个对象如何不实现清理机制,逃脱无所不能的垃圾回收器,并且仍然不会造成任何混乱?这很奇怪!这给人一种印象,我们可能会泄漏底层的本地线程资源。显然,CLR 的开发者不会如此粗心,否则我们今天就不会运行用托管代码编写的应用程序了。我的常识告诉我,背后有一些技巧在起作用,即以某种方式维护着每个线程的引用,从而完成了清理工作。

所以,我和 Ananth 卷起袖子,用 SSCLI 在运行时中寻找证据。从 SSCLI 中挑出一些代码片段来向您展示“这里,这就是证据”是很困难的。然而,我可以分享我们所看到的内容。当一个线程被创建时,对线程对象的引用会被添加到框架维护的一个 static 列表中。因此,无论用户代码是否持有引用,都会建立一个引用。当一个线程启动时,会执行一些框架代码,然后将控制权交给我们的线程委托。当我们的线程委托完成执行(正常或异常)时,它会返回到框架代码中的调用者,由调用者负责从静态列表中移除引用。然后,框架代码会进行一些清理工作,包括关闭线程句柄等。只有到那时,线程对象才成为垃圾回收的候选者,而此时它没有什么特别需要清理的了。我认为 CLR 的开发者(显然)足够聪明,可以这样做。因为

  1. 线程是特殊的资源,其行为与其他本地资源略有不同
  2. Thread 类型只是对原始本地/OS 线程的包装

我们已经看到了证据。现在,让我们谈谈道德问题。当您看到像这样随意启动线程的代码时,您不会怀疑一下吗?难道不会引发关于正确性、安全性等许多问题吗?显然,这不是一个好习惯。仅仅因为框架中有些东西可以处理,并不意味着我们可以为所欲为。同意 SSCLI 不会撒谎,但我们的应用程序代码完全依赖于框架代码的某些非常内在的细节是否完全安全?我认为不是。

SomeMethod 这样的代码似乎是为了启动(一个线程来做一些处理)然后就忘记它了,因为它不关心持有线程对象的引用。而且,SomeMethod 完全可能被调用几次或很多次,而我们将创建新的线程只是为了启动然后忘记。难道我们没有听说过线程是昂贵的资源吗?人们为什么会想到线程池?线程池是适用于这种启动后忘记或不需要线程亲和性的处理的合适选择。

下次您看到类似的代码时,如果您有权更改它,请进行更正。如果不行,请与编写代码的开发人员谈谈。从一个温和友好的谈话开始,向她/他解释道德问题,或者给她/他看这篇文章(一点点营销!)。确保谈话不会变得攻击性(让开发人员产生戒备心理)。即使经过如此友好的谈话,如果开发人员没有脑子站在您这边,那就枪毙他!开玩笑的。

好了,这就是我想分享的内容。现在轮到您评论和/或纠正了。如果您发现或知道任何其他关于线程引用及相关内容的证据,请分享您的想法。我相信这将是一场有趣且有价值的讨论。

© . All rights reserved.