【问题标题】:Why does the use of StreamSocket in a loop cause a memory leak?为什么在循环中使用 StreamSocket 会导致内存泄漏?
【发布时间】:2016-01-02 19:03:33
【问题描述】:

我正在开发一个 C#、UWP 10 解决方案,该解决方案使用快速、连续的读/写循环与网络设备进行通信。 API 提供的 StreamSocket 似乎工作得很好,直到我意识到存在内存泄漏:堆中有 Task<uint32> 的积累,每分钟数百个。

无论我在async Task 中使用普通的旧while (true) 循环,还是使用带有TPL 数据流的自发布ActionBlock<T>(根据this answer),结果都是一样的。

如果我消除从套接字读取并专注于写入,我可以进一步隔离问题: 无论我使用DataWriter.StoreAsync 方法还是更直接的StreamSocket.OutputStream.WriteAsync(IBuffer buffer),问题仍然存在。此外,将.AsTask() 添加到这些没有任何区别。

即使垃圾收集器运行,这些Task<uint32> 也不会从堆中删除。所有这些任务都已完成 (RanToCompletion),没有错误或任何其他表明“尚未准备好回收”的属性值。

似乎在this page 上暗示了我的问题(从托管世界到非托管世界的字节数组阻止了内存的释放),但规定的解决方案似乎非常明显:解决这个问题的唯一方法是编写C++/CX 中的所有通信逻辑。我希望这不是真的;当然,其他 C# 开发人员已经成功实现了无内存泄漏的持续高速网络通信。而且微软肯定不会发布只在 C++/CX 中无内存泄漏工作的 API

编辑

根据要求,一些示例代码。我自己的代码有太多层,但可以使用this Microsoft sample 观察一个更简单的示例。我做了一个简单的修改,循环发送 1000 次以突出问题。这是相关代码:

public sealed partial class Scenario3 : Page
{
    // some code omitted

    private async void SendHello_Click(object sender, RoutedEventArgs e)
    {
        // some code omitted

        StreamSocket socket = //get global object; socket is already connected

        DataWriter writer = new DataWriter(socket.OutputStream);

        for (int i = 0; i < 1000; i++)
        {
            string stringToSend = "Hello";
            writer.WriteUInt32(writer.MeasureString(stringToSend));
            writer.WriteString(stringToSend);
            await writer.StoreAsync();
        }
    }
}

启动应用程序并连接套接字后,堆上只有Task&lt;UInt32&gt; 的实例。单击“SendHello”按钮后,有 86 个实例。第二次按下后:129 次。

编辑#2 在运行我的应用程序(使用紧密循环发送/接收) 3 小时后,我可以看到肯定存在 一个问题:50 万个任务实例,它们永远不会被 GC'd,以及应用程序的进程内存从最初的 46 MB 增加到 105 MB。显然这个应用程序不能无限期地运行。 但是...这仅适用于在调试模式下运行。如果我在 Release 模式下编译我的应用程序,部署并运行它,则不会出现内存问题。我可以让它整夜运行,很明显内存管理得当。 结案。

【问题讨论】:

  • 贴出问题的代码
  • @alexm,请查看发布的代码。谢谢
  • @BCA - 当您在事件处理程序中引用全局对象时,它如何垃圾收集页面?
  • 不是页面,而是任务 实例,我认为它代表 StoreAsync() 调用
  • 澄清一下:我不希望 Page 或 StreamSocket 收集垃圾。但我希望与 StoreAsync() 关联的任务能够以某种方式清理

标签: c# .net async-await winrt-async stream-socket-client


【解决方案1】:

有 86 个实例。第二次按下后:129 次。

这完全正常。并且强烈暗示这里的真正问题是您不知道如何正确解释内存分析器报告。

任务听起来是一个非常昂贵的对象,它物超所值,并且涉及到一个线程,这是您可以创建的最昂贵的操作系统对象。但事实并非如此,Task 对象实际上是一个微不足道的对象。它在 32 位模式下只需要 44 个字节,在 64 位模式下只需要 80 个字节。真正昂贵的资源不属于 Task,由线程池管理器负责。

这意味着您可以在对 GC 堆施加足够压力以触发收集之前创建 很多 个 Task 对象。其中约有 47,000 个以 32 位模式填充第 0 代段。服务器上的更多,数十万,它的段更大。

在您的代码 sn-p 中,Task 对象是您实际创建的唯一对象。因此,您的 for(;;) 循环几乎不会经常循环,以至于无法看到 Task 对象的数量减少或限制。

所以这是通常的故事,对 .NET Framework 存在泄漏的指责,特别是在运行数月的服务器式应用程序中大量使用的这类基本对象类型,永远被高度夸大了。双重猜测垃圾收集器总是很棘手,您通常只有通过让您的应用程序运行数月并且从未在 OOM 上失败才能获得信心。

【讨论】:

  • 非常有趣。我现在正在用我的实际应用程序进行试驾。一个小时后,我有超过 100,000 个任务对象并且还在增加。有趣的是,垃圾收集每隔几秒钟就会发生一次,但这些 Task 对象从未被清理过。但是你说这很正常,如果我不相信我应该让我的应用程序运行到 OOM?
  • 您可以调用GC.Collect 来强制进行垃圾回收,看看它是否会清理这些实例。如果是这样,那么我认为您不必再​​担心这个了。
【解决方案2】:

我会在 for 中创建并关闭 DataWriter。

【讨论】:

  • 我做到了。每次迭代都涉及套接字的 using 块
猜你喜欢
  • 2012-04-10
  • 2012-02-09
  • 2013-07-12
  • 1970-01-01
  • 2017-02-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多