在单独的线程上运行表单时,.NET CF中的内存泄漏

时间:2009-06-17 06:19:51

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

编辑 - 测量完成内存之前的空白线程

这是在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;
}

我是否遗漏了有关处置控制权的问题?有没有人有任何关于从哪里去的想法?

提前致谢。

3 个答案:

答案 0 :(得分:4)

好的,关于这段代码我要说的第一件事就是WTF?!您需要对Windows消息的工作方式进行一些研究。我会在这里介绍一下,但你真的需要先了解它,然后再尝试疯狂的东西,比如我在这里看到的东西。

  1. 创建窗口时,该窗口的所有“操作”都通过Windows消息传递。当您调用Refresh,或Click或其他任何内容时,这是一条Windows消息。
  2. 这些消息是从“消息泵”调度的,它只是一个调用PeekMessage,GetMEsssage,TranslateMessage和DispatchMessage API的循环。
  3. Application.Run在托管代码中执行此操作。
  4. 表单的Windows消息必须在与创建窗口相同的线程上下文中传递。这意味着泵也必须在同一个螺纹上。这就是Control.Invoke存在的原因。
  5. 一个线程上的泵甚至看不到另一个线程上窗口的消息
  6. 所以你肯定会遇到一些问题。我不确定你想要实现的是什么的“大图”以及你如何注意到这个错误,但是这段代码告诉我你的架构中存在一些基本问题。

    但是你认为你发现了这个泄漏怎么样?好吧,没有泄漏。您不了解CF(和托管)内存管理。同样,建议进行研究,但MSDN非常好webcast that covers it well

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

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

    因此,当我们真正了解它时,问题是:你究竟想要解决什么?您正在托管内存环境中运行。除非你看到OOM,否则你几乎总是不应该担心内存水平 - 这就是首先拥有GC的重点。不要尝试建立一个复杂的,在哪里的Waldo学术练习,并让我们试图找到泄漏。

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

    修改

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

答案 1 :(得分:0)

我可能错了,因为我不是CF的专家,但除非系统处于内存压力之下,否则.net将不会释放它所采用的内存。

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

答案 2 :(得分:0)

首先,我会问为什么你要在其他线程上创建足够的表单,你关心的是392个字节,但这就是我想的那个。

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