Обобщая очень полезные комментарии и проведенное исследование:
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?