Когда требуется GC.KeepAlive (this) при выполнении P / Invoke для неуправляемых ресурсов? - PullRequest
2 голосов
/ 01 июня 2019

У меня есть TestNet оболочка для нативного компонента.Собственный компонент предоставляет блокировку TestNative::Foo(), которая связывается с управляемой частью посредством вызова управляемых обратных вызовов, и слабый GCHandle, который используется для получения ссылки на оболочку .NET и предоставляет контекст.GCHandle слабый, поскольку оболочка .NET предназначена для того, чтобы скрыть тот факт, что пользователь обрабатывает неуправляемые ресурсы и намеренно не реализует интерфейс IDisposable: будучи не слабым, он будет препятствовать сбору экземпляров TestNet навсе, создавая утечку памяти.Происходит следующее: в сборке Release только сборщик мусора будет собирать ссылку на оболочку .NET при выполнении управляемого обратного вызова, даже до того, как TestNative::Foo() и неожиданно TestNet::Foo() разблокируется.Я сам понял проблему и могу решить ее, выдав GC.KeepAlive(this) после вызова P / Invoke, но, поскольку знание об этом не очень распространено, многие люди делают это неправильно.У меня есть несколько вопросов:

  1. Всегда ли требуется GC.KeepAlive(this) в управляемом методе, если последней инструкцией является вызов P / Invoke для неуправляемых ресурсов или он просто необходим в этом особом случае, а именно в переключении на управляемыйконтекст выполнения при маршалинге управляемого обратного вызова из нативного кода?Вопрос может быть: я должен поставить GC.KeepAlive(this) везде?Этот старый Microsoft блог (оригинальная ссылка 404, здесь кэшированный ), кажется, предлагает это!Но это изменит игру, и в основном это будет означать, что большинство людей никогда не делали P / Invoke правильно, потому что это потребовало бы пересмотра большинства вызовов P / Invoke в оболочках.Например, есть ли правило, которое говорит, что сборщик мусора ( EDIT : или лучше финализатор) не может работать для объектов, принадлежащих текущему потоку, в то время как контекст выполнения неуправляемый (собственный)?
  2. Где я могу найти соответствующую документацию?Я мог найти политику CodeAnalysis CA2115 , указывающую на общее использование GC.KeepAlive(this) в любой раз, когда неуправляемый ресурс доступен с помощью P / Invoke.В целом GC.KeepAlive(this), кажется, очень редко требуется при работе с финализаторами .
  3. Почему это происходит только в сборке релиза?Похоже, что оптимизация, но она вообще не нужна в сборке Debug, скрывает важное поведение сборщика мусора.

ПРИМЕЧАНИЕ : у меня нет проблем со сборкой делегатов, чтоэто другой вопрос, который я знаю, как правильно решать.Проблема здесь заключается в том, что объекты, содержащие неуправляемые ресурсы, собираются, когда вызовы P / Invoke еще не завершены.

Это следует за кодом, который ясно демонстрирует проблему.Создает консольное приложение C # и проект C ++ Dll1 и создает их в режиме Release :

Program.cs :

using System;
using System.Runtime.InteropServices;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            var test = new TestNet();
            try
            {
                test.Foo();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
        }
    }

    class TestNet
    {
        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        delegate void Callback(IntPtr data);

        static Callback _callback;

        IntPtr _nativeHandle;
        GCHandle _thisHandle;

        static TestNet()
        {
            // NOTE: Keep delegates references so they can be
            // stored persistently in unmanaged resources
            _callback = callback;
        }

        public TestNet()
        {
            _nativeHandle = CreateTestNative();

            // Keep a weak handle to self. Weak is necessary
            // to not prevent garbage collection of TestNet instances
            _thisHandle = GCHandle.Alloc(this, GCHandleType.Weak);

            TestNativeSetCallback(_nativeHandle, _callback, GCHandle.ToIntPtr(_thisHandle));
        }

        ~TestNet()
        {
            Console.WriteLine("this.~TestNet()");
            FreeTestNative(_nativeHandle);
            _thisHandle.Free();
        }

        public void Foo()
        {
            Console.WriteLine("this.Foo() begins");
            TestNativeFoo(_nativeHandle);

            // This is never printed when the object is collected!
            Console.WriteLine("this.Foo() ends");

            // Without the following GC.KeepAlive(this) call
            // in Release build the program will consistently collect
            // the object in callback() and crash on next iteration 
            //GC.KeepAlive(this);
        }

        static void callback(IntPtr data)
        {
            Console.WriteLine("TestNet.callback() begins");
            // Retrieve the weak reference to self. As soon as the istance
            // of TestNet exists. 
            var self = (TestNet)GCHandle.FromIntPtr(data).Target;
            self.callback();

            // Enforce garbage collection. On release build
            self = null;
            GC.Collect();
            GC.WaitForPendingFinalizers();
            Console.WriteLine("TestNet.callback() ends");
        }

        void callback()
        {
            Console.WriteLine("this.callback()");
        }

        [DllImport("Dll1", CallingConvention = CallingConvention.Cdecl)]
        static extern IntPtr CreateTestNative();

        [DllImport("Dll1", CallingConvention = CallingConvention.Cdecl)]
        static extern void FreeTestNative(IntPtr obj);

        [DllImport("Dll1", CallingConvention = CallingConvention.Cdecl)]
        static extern void TestNativeSetCallback(IntPtr obj, Callback callback, IntPtr data);

        [DllImport("Dll1", CallingConvention = CallingConvention.Cdecl)]
        static extern void TestNativeFoo(IntPtr obj);
    }
}

Dll1.cpp :

#include <iostream>

extern "C" typedef void (*Callback)(void *data);

class TestNative
{
public:
    void SetCallback(Callback callback1, void *data);
    void Foo();
private:
    Callback m_callback;
    void *m_data;
};

void TestNative::SetCallback(Callback callback, void * data)
{
    m_callback = callback;
    m_data = data;
}

void TestNative::Foo()
{
    // Foo() will never end
    while (true)
    {
        m_callback(m_data);
    }
}

extern "C"
{
    __declspec(dllexport) TestNative * CreateTestNative()
    {
        return new TestNative();
    }

    __declspec(dllexport) void FreeTestNative(TestNative *obj)
    {
        delete obj;
    }

    __declspec(dllexport) void TestNativeSetCallback(TestNative *obj, Callback callback1, void * data)
    {
        obj->SetCallback(callback1, data);
    }

    __declspec(dllexport) void TestNativeFoo(TestNative *obj)
    {
        obj->Foo();
    }
}

Вывод последовательно:

this.Foo() begins
TestNet.callback() begins
this.callback()
this.~TestNet()
TestNet.callback() ends
TestNet.callback() begins
System.NullReferenceException: Object reference not set to an instance of an object.

Если один комментарий раскомментирует вызов GC.KeepAlive(this) в TestNet.Foo()программа правильно никогда не заканчивается.

1 Ответ

0 голосов
/ 01 июня 2019

Обобщая очень полезные комментарии и проведенное исследование:

1) Всегда ли требуется GC.KeepAlive(this) в методе управляемого экземпляра, если последняя инструкция является вызовом P / Invoke с использованием неуправляемых ресурсов, удерживаемых экземпляром?

Да, если вы не хотите, чтобы пользователь API в последний раз отвечал за хранение не подлежащей сбору ссылки для экземпляра управляемого объекта в патологических случаях, посмотрите пример ниже. Но это не единственный способ: HandleRef или SafeHandle techiniques также могут быть использованы для продления срока службы управляемого объекта при выполнении P / Invoke Interop.

В этом примере впоследствии будут вызываться собственные методы через управляемые экземпляры, содержащие собственные ресурсы:

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            new Thread(delegate()
            {
                // Run a separate thread enforcing GC collections every second
                while(true)
                {
                    GC.Collect();
                    Thread.Sleep(1000);
                }
            }).Start();

            while (true)
            {
                var test = new TestNet();
                test.Foo();
                TestNet.Dump();
            }
        }
    }

    class TestNet
    {
        static ManualResetEvent _closed;
        static long _closeTime;
        static long _fooEndTime;

        IntPtr _nativeHandle;

        public TestNet()
        {
            _closed = new ManualResetEvent(false);
            _closeTime = -1;
            _fooEndTime = -1;
            _nativeHandle = CreateTestNative();
        }

        public static void Dump()
        {
            // Ensure the now the object will now be garbage collected
            GC.Collect();
            GC.WaitForPendingFinalizers();

            // Wait for current object to be garbage collected
            _closed.WaitOne();
            Trace.Assert(_closeTime != -1);
            Trace.Assert(_fooEndTime != -1);
            if (_closeTime <= _fooEndTime)
                Console.WriteLine("WARN: Finalize() commenced before Foo() return");
            else
                Console.WriteLine("Finalize() commenced after Foo() return");
        }

        ~TestNet()
        {
            _closeTime = Stopwatch.GetTimestamp();
            FreeTestNative(_nativeHandle);
            _closed.Set();
        }

        public void Foo()
        {
            // The native implementation just sleeps for 250ms
            TestNativeFoo(_nativeHandle);

            // Uncomment to have all Finalize() to commence after Foo()
            //GC.KeepAlive(this);
            _fooEndTime = Stopwatch.GetTimestamp();
        }

        [DllImport("Dll1", CallingConvention = CallingConvention.Cdecl)]
        static extern IntPtr CreateTestNative();

        [DllImport("Dll1", CallingConvention = CallingConvention.Cdecl)]
        static extern void FreeTestNative(IntPtr obj);

        [DllImport("Dll1", CallingConvention = CallingConvention.Cdecl)]
        static extern void TestNativeFoo(IntPtr obj);
    }
}

Чтобы родной вызов был всегда безопасным, мы ожидаем, что финализатор будет вызываться только после возврата Foo(). Вместо этого мы можем легко принудить нарушения, вручную вызывая сборку мусора в фоновом потоке. Вывод следующий:

Finalize() commenced after Foo() return
WARN: Finalize() commenced before Foo() return
Finalize() commenced after Foo() return
Finalize() commenced after Foo() return
Finalize() commenced after Foo() return
WARN: Finalize() commenced before Foo() return
Finalize() commenced after Foo() return

2) Где я могу найти документацию?

Документация GC.KeepAlive() предоставляет пример, очень похожий на управляемый обратный вызов в исходном вопросе. HandleRef также имеет очень интересные соображения относительно жизненного цикла управляемых объектов и взаимодействия:

Если вы используете платформу invoke для вызова управляемого объекта, а объект не упоминается в другом месте после вызова платформы, это возможно для сборщика мусора завершить управляемый объект. Это действие освобождает ресурс и делает недействительным дескриптор, вызывая платформа вызывает вызов для сбоя. Заворачивание ручки с помощью HandleRef гарантирует, что управляемый объект не будет собирать мусор до вызов вызова платформы завершен.

Также ссылка [1], найденная @GSerg, объясняет, когда объект может быть выбран для сбора, указывая, что ссылка this отсутствует в корневом наборе, что позволяет собирать ее также, когда метод экземпляра не возвращен.

3) Почему это происходит только в сборке релиза?

Это оптимизация и может происходить также в сборке Debug с включенной оптимизацией, как указано @SimonMourier. Он также не включен по умолчанию в Debug, поскольку он может предотвратить отладку переменных в текущей области действия метода, как описано в этих other answers .

* +1045 * [1] https://devblogs.microsoft.com/oldnewthing/20100810-00/?p=13193?
...