Memory leaks? in passing IEnumerable array t

2019-09-11 03:12发布

问题:

Is that the correct way to allocate and free handles to managed data passed to unmanaged dll?

There is unmanaged dll with exported function

void Function(byte** ppData, int N);

I need to pass it IEnumerable<byte[]> afids

var handles = afids.Select(afid => GCHandle.Alloc(afid, GCHandleType.Pinned));
var ptrs = handles.Select(h => h.AddrOfPinnedObject());
IntPtr[] afidPtrs = ptrs.ToArray();
uint N = (uint)afidPtrs.Length;

Function(afidPtrs, N);

handles.ToList().ForEach(h => h.Free());

I get managed memory leaks and getting sos.dll in Immediate Window gave gcroot

DOMAIN(00275030):HANDLE(Pinned):3ea2c0:Root:  17a8d190(System.Byte[])

Function definition is:

[DllImport("My.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
static extern unsafe internal int Function(IntPtr[] ppData, int N);

Buggy code snippet for console application:

static void Main(string[] args)
{
    while (!Console.KeyAvailable)
    {
        IEnumerable<byte[]> data = CreateEnumeration(100);
        PinEntries(data);
        Thread.Sleep(900);
        Console.Write(String.Format("gc mem: {0}\r", GC.GetTotalMemory(true)));
    }
}

static IEnumerable<byte[]> CreateEnumeration(int size)
{
    Random random = new Random();
    IList<byte[]> data = new List<byte[]>();
    for (int i = 0; i < size; i++)
    {
        byte[] vector = new byte[12345];
        random.NextBytes(vector);
        data.Add(vector);
    }
    return data;
}

static void PinEntries(IEnumerable<byte[]> data)
{
    var handles = data.Select(d => GCHandle.Alloc(d, GCHandleType.Pinned));
    var ptrs = handles.Select(h => h.AddrOfPinnedObject());
    IntPtr[] dataPtrs = ptrs.ToArray();
    Thread.Sleep(100); // unmanaged function call taking byte** data
    handles.ToList().ForEach(h => h.Free());
}

Correct code snippet for console application:

static void PinEntries(IEnumerable<byte[]> data)
{
    IEnumerable<GCHandle> handles = CreateHandles(data);
    IntPtr[] ptrs = GetAddrOfPinnedObjects(handles);
    Thread.Sleep(100); // unmanaged function call taking byte** data
    FreeHandles(handles);
}

static IEnumerable<GCHandle> CreateHandles(IEnumerable<byte[]> data)
{
    IList<GCHandle> handles = new List<GCHandle>();
    foreach (byte[] vector in data)
    {
            GCHandle handle = GCHandle.Alloc(vector, GCHandleType.Pinned);
            handles.Add(handle);
    }
    return handles;
}

static IntPtr[] GetAddrOfPinnedObjects(IEnumerable<GCHandle> handles)
{
    IntPtr[] ptrs = new IntPtr[handles.Count()];
    for (int i = 0; i < ptrs.Length; i++)
            ptrs[i] = handles.ElementAt(i).AddrOfPinnedObject();
    return ptrs;
}

static void FreeHandles(IEnumerable<GCHandle> handles)
{
    foreach (GCHandle handle in handles)
            handle.Free();
}

回答1:

static void PinEntries(IEnumerable<byte[]> data)
{
    var handles = data.Select(d => GCHandle.Alloc(d, GCHandleType.Pinned));
    var ptrs = handles.Select(h => h.AddrOfPinnedObject());
    IntPtr[] dataPtrs = ptrs.ToArray();
    Thread.Sleep(100); // unmanaged function call taking byte** data
    handles.ToList().ForEach(h => h.Free());
}

You are falling into a Linq trap, its enumerators don't behave like a collection. The handles get allocated twice, first when you use ptrs.ToArray(), again when you use handles.ToList(). With the obvious side-effect that the first set of handles don't get freed. Fix:

        var handles = data.Select(d => GCHandle.Alloc(d, GCHandleType.Pinned)).ToList();
        var ptrs = handles.Select(h => h.AddrOfPinnedObject());
        IntPtr[] dataPtrs = ptrs.ToArray();
        handles.ForEach(h => h.Free());

Note the added ToList() to force the enumeration into a collection.