【问题标题】:Memory Leaks in .NET CF when running forms on separate threads在单独的线程上运行表单时 .NET CF 中的内存泄漏
【发布时间】:2009-06-17 06:19:50
【问题描述】:

EDIT - 在测量完成内存之前清空线程

这都是在 Windows CE 5.0 下运行 .NET Compact Framework 2.0。

我在开发我的应用程序时遇到了一些有趣的行为。每当我尝试创建一个表单并让它运行一个单独的线程时,它似乎在关闭时泄漏了 392 个字节,我不确定为什么。

到目前为止,我的方法是创建一个新线程并让它 a) 创建表单并不断调用 Application.DoEvents 直到它关闭或 b) 创建表单并将其传递给 Application.Run(Form)。

以下是说明问题的示例表单。

public partial class TestForm : Form
{
    public TestForm()
    {
        InitializeComponent();
    }

    private void DoMemoryTest(bool useApplicationRun)
    {
        GC.WaitForPendingFinalizers();
        GC.Collect();
        long originalMem = GC.GetTotalMemory(true);

        Thread t;
        if (useApplicationRun)
            t = new Thread(new ThreadStart(AppRunThread));
        else
            t = new Thread(new ThreadStart(DoEventThread));
        t.Start();

        Thread.Sleep(3000);//Dodgey hack
        t.Join();
        t = null;

        GC.WaitForPendingFinalizers();
        GC.Collect();
        long terminatingMem = GC.GetTotalMemory(true);

        MessageBox.Show(String.Format("An increase of {0} bytes was measured from {1} bytes", 
                        terminatingMem - originalMem, originalMem));
    }

    private void button1_Click(object sender, EventArgs e)
    {
        DoMemoryTest(false);
    }

    private void button2_Click(object sender, EventArgs e)
    {
        DoMemoryTest(true);
    }

    private void AppRunThread()
    {
        Application.Run(new OpenCloseForm());
    }

    private void DoEventThread()
    {
        using (OpenCloseForm frm = new OpenCloseForm())
        {
            frm.Show();
            do
            {
                Application.DoEvents();
            } while (frm.Showing);
        }
    }

    /// <summary>
    /// Basic form that opens for a short period before shutting itself
    /// </summary>
    class OpenCloseForm : Form
    {
        public OpenCloseForm()
        {
            this.Text = "Closing Soon";
            this.Size = new Size(100, 100);
            this.TopMost = true;
        }

        public volatile bool Showing = false; //dodgy hack for DoEventThread

        System.Threading.Timer timer;
        protected override void OnLoad(EventArgs e)
        {
            Showing = true;
            base.OnLoad(e);
            timer = new System.Threading.Timer(new TimerCallback(TimerTick), null, 1000, 1000);
        }

        delegate void CloseDelegate();
        private void TimerTick(object obj)
        {
            this.Invoke(new CloseDelegate(this.Close));
        }

        protected override void OnClosed(EventArgs e)
        {
            base.OnClosed(e);
            Showing = false;
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (timer != null)
                {
                    timer.Dispose();
                    timer = null;
                }
            }
            base.Dispose(disposing);
        }
    }

    //Designer code to follow....

    /// <summary>
    /// Required designer variable.
    /// </summary>
    private System.ComponentModel.IContainer components = null;

    /// <summary>
    /// Clean up any resources being used.
    /// </summary>
    /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
    protected override void Dispose(bool disposing)
    {
        if (disposing && (components != null))
        {
            components.Dispose();
        }
        base.Dispose(disposing);
    }

    #region Windows Form Designer generated code

    /// <summary>
    /// Required method for Designer support - do not modify
    /// the contents of this method with the code editor.
    /// </summary>
    private void InitializeComponent()
    {
        this.timer1 = new System.Windows.Forms.Timer();
        this.button1 = new System.Windows.Forms.Button();
        this.button2 = new System.Windows.Forms.Button();
        this.SuspendLayout();
        // 
        // button1
        // 
        this.button1.Location = new System.Drawing.Point(32, 47);
        this.button1.Name = "button1";
        this.button1.Size = new System.Drawing.Size(116, 39);
        this.button1.TabIndex = 1;
        this.button1.Text = "DoEvents Loop";
        this.button1.Click += new System.EventHandler(this.button1_Click);
        // 
        // button2
        // 
        this.button2.Location = new System.Drawing.Point(32, 115);
        this.button2.Name = "button2";
        this.button2.Size = new System.Drawing.Size(116, 39);
        this.button2.TabIndex = 2;
        this.button2.Text = "Application.Run";
        this.button2.Click += new System.EventHandler(this.button2_Click);
        // 
        // TestForm
        // 
        this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
        this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
        this.AutoScroll = true;
        this.ClientSize = new System.Drawing.Size(177, 180);
        this.Controls.Add(this.button2);
        this.Controls.Add(this.button1);
        this.Name = "TestForm";
        this.Text = "TestForm";
        this.TopMost = true;
        this.ResumeLayout(false);

    }

    #endregion

    private System.Windows.Forms.Timer timer1;
    private System.Windows.Forms.Button button1;
    private System.Windows.Forms.Button button2;
}

我是否遗漏了有关控件处置的某些内容?有没有人对此有任何想法?

提前致谢。

【问题讨论】:

    标签: c# .net compact-framework memory-leaks


    【解决方案1】:

    好吧,关于这段代码,我首先要说的是WTF?!您需要对 Windows 消息的工作方式进行一些研究。我会在这里介绍一点,但在潜入并尝试像我在这里看到的那样疯狂的东西之前,你真的需要理解它。

    1. 创建窗口后,该窗口的所有“操作”都通过 Windows 消息传递。当您调用 Refresh 或 Click 或其他方法时,这是一条 Windows 消息。
    2. 这些消息是从“消息泵”发送的,它只是一个调用 PeekMessage、GetMEsssage、TranslateMessage 和 DispatchMessage API 的循环。
    3. Application.Run 在托管代码中执行此操作。
    4. 窗体的 Windows 消息必须在创建窗口时在同一线程上下文中传递。这意味着泵也必须在同一线程上。这就是 Control.Invoke 存在的原因。
    5. 一个线程上的泵甚至看不到另一个线程上窗口的消息

    因此,您所做的事情肯定存在一些问题。我不确定你想要实现的“大图”是什么,以及你是如何注意到这个错误的,但这段代码告诉我你的架构中存在一些基本问题。

    但是你认为你已经找到了这个漏洞呢?好吧,没有泄漏。您不了解 CF(和托管)内存管理。同样,建议进行研究,但 MSDN 有一个非常好的webcast that covers it well

    这个场景的缺点是你在不同的线程上创建了一些对象。这些线程创建了一堆东西,其中一些是 IDisposable,有些不是。当线程断开时,这些项目不再有根,因此可以收集。当调用 Collect 时,GC 会遍历所有根并记录每个具有根(标记)的对象。那些没有的则被“释放”(清扫)。它们所在的 GC 堆中的区域不再标记为正在使用 - 有关详细信息,请参阅网络广播。如果该项目实现了 IDisposabe,则它获得一个新的根,因此终结器仍然可以存在并在下一个 GC 循环中运行。最后,终结器线程运行(非确定性)。

    您的代码没有考虑到这种行为。您没有运行 Collect 两次。在 Collect 调用之后,您没有等待终结器(仅调用 WaitForPendingFinalizers 可能还不够)。由于你的线程本身没有被标记为后台线程,谁知道它们在生命周期中的位置和 GC 使用状态可能是什么。

    所以当我们真正开始着手时,问题是:您到底想解决什么问题?您正在托管内存环境中运行。除非您看到 OOM,否则您几乎总是不应该担心内存级别 - 这就是首先拥有 GC 的全部意义。不要试图建立一个错综复杂的 Waldo 学术活动,让我们尝试找出漏洞。

    如果您确实遇到了问题,那么您应该首先确保您的应用程序设计符合 Windows 应用程序的编写方式,然后使用tools like RPM 来分析哪些根占用了内存并修复泄漏您已经创建(是的,在托管代码中仍然可以而且确实会发生泄漏)。当然,您也可以在这里就现实世界问题提出合理的问题。

    编辑

    微软似乎已经清除了我上面提到的网络广播内容。希望他们能够找到并重新发布它,但与此同时(如果他们永远找不到的话)我至少有我在 MEDC 和it's available on my blog 的原始演讲中使用的 PowerPoint。

    【讨论】:

    • 感谢您的回复。我试图解决的问题是有一个“系统很忙”的动画形式,我可以在程序正在做一些密集的事情时显示它。虽然我知道我可以通过回调加载表单来完成此操作,但我希望将此表单与实际的计算代码分开,从而在单独的线程上制作一个动画表单。在试图做到这一点时,我注意到记忆消失了,并对正在发生的事情感到好奇。我也会看看那个网络广播。
    【解决方案2】:

    我可能错了,因为我不是 CF 方面的专家,但 .net 不一定会释放它占用的内存,除非系统处于内存压力之下。

    我的理解是,如果应用程序需要 x 字节的内存一次,它可能会在某个时候再次需要至少 x 字节。除非操作系统中的其他东西需要它,否则它不会释放该内存。

    【讨论】:

    • 我知道你对桌面 CLR 上的 GC 行为是正确的(将内存还给操作系统),但我相信 GC.GetTotalMemory 只返回 .NET 实际使用的数据,而不是它的数量已从操作系统分配。
    • 这种类型的 JIT 会不会有问题?即当你检查它没有编译但是当你调用它时?每次点击都会泄漏吗?
    • 我同意乔纳森的观点,我所有的测试似乎都与垃圾收集器关于框架分配了多少内存的想法一致,而不是 GlobalMemoryStatus 报告的实际物理内存占用量。
    • 是的,每次我点击它都会泄漏(偶尔会泄漏一点,但我认为这与我点击太快有关)。
    【解决方案3】:

    首先,我会问你为什么要在其他线程上创建足够多的表单,而你关心 392 字节,但我猜这不是重点。

    我要开始的第一个地方是您没有在 DoMemoryTest 方法中删除的托管线程实例。在 Thread.Sleep(3000) 调用之后调用 t.Join(),然后将其设置为 null(因此即使在调试模式下也可以进行 GC)。

    【讨论】:

    • 糟糕,这是一个愚蠢的疏忽。不幸的是,在清除它之后,泄漏现在只减少到 336 个字节。我主要关心的是因为我担心我以某种方式错过了 .NET CF 的一些基本概念,我不希望它以后会咬我。感谢您的回复。
    猜你喜欢
    • 2023-03-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-08-14
    • 2015-06-23
    • 2010-09-15
    • 2016-02-26
    • 1970-01-01
    相关资源
    最近更新 更多