该线程如何导致内存泄漏?

时间:2018-08-10 09:20:13

标签: c# multithreading memory-leaks

我们的一个程序遭受了严重的内存泄漏:在客户站点,其进程内存每天增加1 GB。 我可以在测试中心中设置该方案,每天可能会发生大约700 MB的内存泄漏。

此应用程序是用C#编写的Windows服务,可通过CAN总线与设备通信。

内存泄漏不取决于应用程序写入CAN总线的数据速率。但这显然取决于收到的消息数。

阅读邮件的“不受管理”方面是:

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct CAN_MSG
{
    public uint time_stamp;
    public uint id;
    public byte len;
    public byte rtr;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
    public byte[] a_data;
}

[DllImport("IEICAN02.dll", EntryPoint = "#3")]
public static extern int CAN_CountMsgs(ushort card_idx, byte can_no, byte que_type);
//ICAN_API INT32 _stdcall CAN_CountMsgs(UINT16 card_idx, UINT8 can_no,UINT8 que_type);

[DllImport("IEICAN02.dll", EntryPoint = "#10")]
public static extern int CAN_ReadMsg(ushort card_idx, byte can_no, ushort count, [MarshalAs(UnmanagedType.LPArray), Out()] CAN_MSG[] msg);
//ICAN_API INT32 _stdcall CAN_ReadMsg(UINT16 card_idx, UINT8 can_no, UINT16 count, CAN_MSG* p_obj);

我们基本上使用以下方式:

private void ReadMessages()
{
    while (keepRunning)
    {
        // get the number of messages in the queue
        int messagesCounter = ICAN_API.CAN_CountMsgs(_CardIndex, _PortIndex, ICAN_API.CAN_RX_QUE);
        if (messagesCounter > 0)
        {
            // create an array of appropriate size for those messages
            CAN_MSG[] canMessages = new CAN_MSG[messagesCounter];
            // read them
            int actualReadMessages = ICAN_API.CAN_ReadMsg(_CardIndex, _PortIndex, (ushort)messagesCounter, canMessages);
            // transform them into "our" objects
            CanMessage[] messages = TransformMessages(canMessages);
            Thread thread = new Thread(() => RaiseEventWithCanMessages(messages))
            {
                Priority = ThreadPriority.AboveNormal
            };
            thread.Start();
        }
        Thread.Sleep(20);
    }
}

 // transformation process:
new CanMessage
{
    MessageData = (byte[])messages[i].a_data.Clone(),
    MessageId = messages[i].id
};

循环大约每30毫秒执行一次。

当我在同一线程中调用RaiseEventWithCanMessages(messages)时,内存泄漏消失了(嗯,不是全部,每天大约有10 MB,即大约原始泄漏的1%),但是其他泄漏很可能是不相关的)。

我不明白线程的这种创建如何导致内存泄漏。您能为我提供一些信息是如何引起内存泄漏的吗?

附录2018-08-16: 该应用程序从大约50 MB的内存开始,然后崩溃到大约2GB。这意味着,大部分时间里都可以使用千兆字节的内存。 另外,CPU大约占20%-4个内核中有3个处于空闲状态。 应用程序使用的线程数在大约30个线程左右保持相当恒定。 总体而言,垃圾收集有很多可用资源。仍然,GC失败。

每秒约有30个线程,每天内存泄漏700 MB,每个新创建的线程平均约有300个字节的内存泄漏;每个新线程大约有5条消息,每条消息大约60字节。 “非托管”结构不会使其进入新线程,而是将其内容复制到新实例化的类中。

所以:为什么尽管有大量可用资源,GC还是会失败?

1 个答案:

答案 0 :(得分:3)

您每隔30毫秒创建2个数组和一个线程,它们之间没有任何协调。数组可能是个问题,但是坦率地说,我很多更担心线程-创建线程真的非常昂贵。您应该经常创建它们。

我还担心如果读取循环超出线程的速度(即RaiseEventWithCanMessages比执行查询/睡眠的代码花费更多的时间)会发生什么。在这种情况下,线程将不断增长。而且您可能还会与所有其他RaiseEventWithCanMessages互相争斗。

RaiseEventWithCanMessages内联“修复”的事实表明,这里的主要问题是要么创建的线程数量过多(坏),要么许多重叠的和不断增长的 并发 RaiseEventWithCanMessages


最简单的解决方法是:不要在此处使用多余的线程。

如果您实际上想要并发操作,那么我这里恰好有两个线程-一个执行查询的线程,一个执行任何RaiseEventWithCanMessages的线程,都在一个循环中。然后,我将在线程之间进行协调,以使查询线程等待完成上一个RaiseEventWithCanMessages,以便以协调的方式移交它-因此始终最多一个出色的RaiseEventWithCanMessages,如果查询没有跟上,您将停止运行查询。

本质上:

CanMessage[] messages = TransformMessages(canMessages);
HandToConsumerBlockingUntilAvailable(messages); // TODO: implement

其他线程基本上在做:

var nextMessages = BlockUntilAvailableFromProducer(); // TODO: implement

一个非常基本的实现可能只是:

void HandToConsumerBlockingUntilAvailable(CanMessage[] messages) {
    lock(_queue) {
        if(_queue.Length != 0) Monitor.Wait(_queue); // block until space
        _queue.Enqueue(messages);
        if(queue.Length == 1) Monitor.PulseAll(_queue); // wake consumer
    }
}
CanMessage[] BlockUntilAvailableFromProducer() {
    lock(_queue) {
        if(_queue.Length == 0) Monitor.Wait(_queue); // block until work
        var next = _queue.Dequeue();
        Monitor.Pulse(_queue); // wake producer
        return _next;
    }
}
private readonly Queue<CanMessage[]> _queue = new Queue<CanMessage[]>;

此实现要求队列中未处理的Message[]待处理的RaiseEventWithCanMessages数量不得超过

这解决了创建大量线程的问题,查询循环超出了ArrayPool<T>.Shared代码的问题。

我可能还会 考虑使用pip install flask-mysql 来租用超大数组(这意味着:您需要注意不要读取比您多的数据实际写出来的,因为您可能要求的数组是500,但是大小为512),而不是不断分配数组。