Memory leak with ConcurrentQueue

2019-04-24 22:45发布

问题:

i have memory leak when using ConcurrentQueue :

requestObject request = xxx;

Item obj= new Item ();
obj.MessageReceived += obj_MessageReceived;
obj.Exited += obj_Exited;

request.Key = obj.Key;

obj.AddRequest(request);

_queue.TryAdd(obj.Key, obj);

In the "Exited" callback, i dispose the resource :

void LiveSphere_Exited(string key)
{
    Item instance;

    _queue.TryRemove(key, out instance);

    Task.Factory.StartNew(() =>
    {
        var wait = new SpinWait();
        while (instance.MessageCount > 0)
        {
            wait.SpinOnce();
        }
    })
    .ContinueWith((t) =>
    {
         if (instance != null)
         {
             //Cleanup resources
             instance.MessageReceived -= obj_MessageReceived;
             instance.Exited -= obj_Exited;
             instance.Dispose();
             instance = null;
         }
    });
}

When I profile the code, i still have a root referenced "Item" object but I don't know where I can dispose..., The exited method is triggered and the _queue has removed the "Item" object from the queue.

When I read documentation, the concurrentqueue copy the reference into the queue.

Can you help me to find out where the memory leak is?

回答1:

Unlike a standard .NET Queue, calling Dequeue() does not remove the reference to the object from the collection. While this behavior has changed from the 4.0 version to the 4.5 version (I have read this, but have not tested it) it is not a bug, but a conscious design decision made by the framework team as a part of designing a thread-safe, enumerable collection.

This article has more information, including a workaround by using StrongBox to wrap the objects that go into the ConcurrentQueue. That should be a suitable work-around until you can move to the 4.5 framework.



回答2:

I have looked implementation of the Concurrent Queue. There are cases when the queue will hold references to the object after Dequeue() was called.

Concurrent Queue uses Segments to store data. There it is a part of the TryRemove method of the segment:

// If there is no other thread taking snapshot (GetEnumerator(), ToList(), etc), reset the deleted entry to null.
// It is ok if after this conditional check m_numSnapshotTakers becomes > 0, because new snapshots won't include 
// the deleted entry at m_array[lowLocal]. 
if (m_source.m_numSnapshotTakers <= 0)
{
    m_array[lowLocal] = default(T); //release the reference to the object. 
} 

So when you have a different thread that enumerates the queue at the same time you dequeue an object references to the object will not be set to null.