10

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

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

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

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

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

编辑

根据要求,一些示例代码。我自己的代码有太多层,但是可以通过这个 Microsoft 示例观察到一个更简单的示例。我做了一个简单的修改,循环发送 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<UInt32>。单击“SendHello”按钮后,有 86 个实例。第二次按下后:129 次。

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

4

2 回答 2

10

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

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

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

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

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

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

于 2015-10-10T14:30:48.367 回答
0

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

于 2015-10-14T16:11:02.530 回答