.Net线程中的句柄泄漏问题

7

这个问题其实困扰了我好几次。如果你写的代码像这样简单:

    private void button1_Click(object sender, EventArgs e)
    {

        for (int i = 0; i < 1000000; i++)
        {
            Thread CE = new Thread(SendCEcho);
            CE.Priority = ThreadPriority.Normal;
            CE.IsBackground = true;
            CE.Start();
            Thread.Sleep(500);
            GC.Collect();
        }
    }

    private void SendCEcho()
    {
        int Counter = 0;

        for (int i = 0; i < 5; i++)
        {
            Counter++;
            Thread.Sleep(25);
        }
    }

运行这段代码并观察句柄的变化!Thread.Sleep是为了让你能够关闭它并防止它占用电脑。这应该保证启动的线程在下一个线程启动之前结束。调用GC.Collect(); 没有效果。从我的观察来看,这段代码每次在任务管理器上正常刷新时都会丢失10个句柄。
SendCEcho()函数中的内容不重要,你可以数到五。当线程死掉时,有一个句柄没有被清除。
对于大多数程序而言,这并不重要,因为它们不会长时间运行。但在我创建的一些程序中,它们需要长时间运行几个月甚至更长时间。
如果你反复执行这段代码,最终可能会泄漏句柄,导致Windows操作系统变得不稳定并无法使用。此时需要重新启动。
我通过使用线程池和类似以下的代码解决了这个问题:
ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadJobMoveStudy));

我的问题是为什么.NET会出现这样的泄漏问题,而且它已经存在这么长时间了?自1.0以来一直存在?我在这里找不到答案。


2
你是通过什么方式查看句柄的? - Yacoub Massad
我没有看到你所描述的行为。你确定在SendCEcho中没有泄漏任何东西吗? - Brian Rasmussen
你正在创建太多的线程。 - Yacoub Massad
这应该可以通过只使用几个线程来重现。你能在每个线程退出后调用 GC.Collect() 并查看句柄数量是否下降吗? - Yacoub Massad
4
你正在创建永远不会结束的线程。SendCEcho 中的循环是无限的。 - Brian Rasmussen
显示剩余9条评论
3个回答

5

您正在创建永远不会结束的线程。在 SendCEcho 中的 for 循环永远不会终止,因此线程永远不会结束,也因此无法被回收。如果我修复您的循环代码,那么线程就会正常结束并被回收。我无法重现以下代码中的问题。

static void SendCEcho()
{
    int Counter = 0;

    for (int i = 0; i < 5; i++)
    {
        Counter++;
        Thread.Sleep(25);
    }
}

你的初始代码是 for (int i = 0; i < 5 + i++) - 这将永远不会终止。你更正后的代码无法编译。这里的重点是线程方法的内容确实很重要。 - Brian Rasmussen

5
    for (int i = 0; i < 5 + i++; )

相当奇怪的拼写错误,你没有重新创建你真正的问题。有一个问题,一个线程对象消耗5个操作系统句柄,它的内部终结器释放它们。.NET类通常有一个Dispose()方法来确保这些句柄可以尽早释放,但Thread没有这样的方法。这是一种勇敢的设计,这样的Dispose()方法会很难调用。
因此,必须依赖终结器是一个严格的要求。在一个只有一个“SendEcho”方法而不做其他事情的程序中,你正在冒着垃圾收集器永远不运行的风险。所以终结器无法完成其工作。然后就由你自己调用GC.Collect()。你可以考虑每启动1000个线程时这样做。或者使用ThreadPool.QueueUserWorkItem()或Task.Run()来回收线程,这是逻辑上的方法。
使用Perfmon.exe验证GC确实没有运行。为你的程序添加.NET CLR Memory > # Gen 0 Collections计数器。

抱歉,我打错了一个字母,但这只是一个for循环。 - Jake

4
尝试在调用GC.Collect()后添加GC.WaitForPendingFinalizers(); 我认为这样可以达到您想要的效果。完整源代码如下:

编辑

另外,看一下这个来自2005年的链接:https://bytes.com/topic/net/answers/106014-net-1-1-possibly-1-0-also-threads-leaking-event-handles-bug。几乎与您的代码完全相同。

using System;
using System.Threading;

class Program
{
    static void Main(string[] args)
    {
        for (int i = 0; i < 1000000; i++)
        {
            Thread CE = new Thread(SendCEcho);
            CE.Priority = ThreadPriority.Normal;
            CE.IsBackground = true;
            CE.Start();
            Thread.Sleep(500);
            CE = null;
            GC.Collect();
            GC.WaitForPendingFinalizers();
        }
    }

    public static void SendCEcho()
    {
        int Counter = 0;
        for (int i = 0; i < 5; i++ )
        {
            Counter++;
            Thread.Sleep(25);
        }
    }
}

好的,我重新运行了一些测试,看起来你是正确的。如果我运行这段代码并添加GC.WaitForPendingFinalizers();,我就没有句柄泄漏了。那么问题就变成了为什么我必须显式调用这个函数或者Windows不释放资源?我以为垃圾回收会自动进行,至少最终会进行。显然不是这样,因为没有这个调用,它最终会导致操作系统崩溃。 - Jake
2
Collect() 方法将运行垃圾回收,但可终止对象将被放入终结队列。它们的终结器只会在未来某个时间点由终结线程运行。调用 WaitForPendingFinalizers() 方法会阻塞当前线程,直到终结线程完成其工作。 - dlev
@dlev 答案简单明了,但完美无瑕。谢谢! - Jake

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