GC 从我下面收集本地对象

GC collects local object right out from under me

我希望有人能看一眼并向我解释我错过了什么。

简短的版本是我在一个函数中创建一个托管对象并在其上调用一个成员。在那个成员函数甚至还没有返回之前,GC 就从我下面的另一个线程中完成了这个对象。

这是创建对象并调用 C++/CLI 函数的 C# 代码

var integrator = Integrator.Create();
_logger.Debug("Integrating normal map.  Res=" + model.ResolutionMmpp);
var hm = integrator.IntegrateNormalMap(nm, model.ResolutionMmpp);

// *** 'integrator' gets finalized before I even get here.  How? ***
_logger.Debug("Back from integrate");  

这里是IntegrateNormalMap的实现(C++/CLI代码)。它调用其非托管等效 C++ 代码。

HeightMap^ Integrator::IntegrateNormalMap(NormalMap^ nrm, double res)
{
    //    Note:  'm_psi' is a pointer to the valid, unmanaged C++ object
    return gcnew HeightMap(m_psi->integrateNormalMap(nrm->sdkMap(), res));
}

我在 Integrator class 终结器(即 Integrator::!Integrator)上放置了一个断点,我可以看到垃圾收集器从不同的线程调用我的 C++/CLI 对象的终结器。

这里是被调用的finalizer的调用栈

Sdk::Integrator::~Integrator() Line 26  C++
Sdk::Integrator::Dispose()  C++
Sdk::Integrator::Dispose()  C++
Sdk::Integrator::!Integrator() Line 38  C++
Sdk::Integrator::Dispose()  C++
[Native to Managed Transition]  
00007ffeee324034()  Unknown
00007ffeee473691()  Unknown

但与此同时,IntegrateNormalMap() 函数仍在原始线程上 运行。

gs::detail::PoissonIntegratorV2014::reconstructNormals(normals={...}) Line 79   C++
gs::detail::PoissonIntegratorV2014::integrateNormalMap(nrm, res) Line 35    C++
[Managed to Native Transition]  
Sdk::Integrator::IntegrateNormalMap(nrm, res) Line 45   C++
Mobile.ViewModels.ScanVm.Generate3d(ffcEnum, token) Line 595    C#
Mobile.ViewModels.ScanVm.Generate3d(token) Line 642 C#
Capture.ViewModels.NormalScanJob.Generate3d() Line 60   C#
Capture.ViewModels.CaptureVm.get_Mesh.AnonymousMethod__27_4(job = {Capture.ViewModels.NormalScanJob}) Line 371  C#
System.Threading.Tasks.Dataflow.TransformBlock<Capture.ViewModels.NormalScanJob, Capture.ViewModels.NormalScanJob>.ProcessMessage(transform, messageWithId = {[{Capture.ViewModels.NormalScanJob}, 0]}) Unknown
System.Threading.Tasks.Dataflow.TransformBlock<System.__Canon, System.__Canon>..ctor.AnonymousMethod__3(messageWithId)  Unknown
System.Threading.Tasks.Dataflow.Internal.TargetCore<Capture.ViewModels.NormalScanJob>.ProcessMessagesLoopCore() Unknown
System.Threading.Tasks.Task.Execute()   Unknown
System.Threading.ExecutionContext.RunInternal(executionContext, callback, state, preserveSyncCtx)   Unknown
System.Threading.ExecutionContext.Run(executionContext, callback, state, preserveSyncCtx)   Unknown
System.Threading.Tasks.Task.ExecuteWithThreadLocal(currentTaskSlot = Id = 2494, Status = Running, Method = Inspecting the state of an object in the debuggee of type System.Delegate is not supported in this context.) Unknown
System.Threading.Tasks.Task.ExecuteEntry(bPreventDoubleExecution)   Unknown
System.Threading.ThreadPoolWorkQueue.Dispatch() Unknown
[Native to Managed Transition]  
00007ffeee324034()  Unknown
00007ffeee473691()  Unknown

请注意,非托管 C++ 代码没有做任何奇怪的事情。

我确实偶然发现了奇怪的 "fix" 但我不相信它。我不认为这真的是一个解决方案,我不明白为什么它可以防止这个问题:

如果我在托管 C++/CLI IntegrateNormalMap 函数中放置一个 try/catch 框架(以使其将任何非托管 C++ 异常转换为托管异常),问题就会消失。在这里,用 try/catch

重写
HeightMap^ Integrator::IntegrateNormalMap(NormalMap^ nrm, double res)
{
    try
    {
      return gcnew HeightMap(m_psi->integrateNormalMap(nrm->sdkMap(), res));
    }
    catch (const std::exception& ex)
    {
        throw gcnew SdkException(ex.what());
    }
}

注意:虽然这显然是良好的通用做法(即防止非托管异常转义),但在任何一种情况下,底层非托管代码实际上都不会抛出任何异常。还在处理中。

我还验证了我 使用 /clr 选项编译此 C++/CLI class。它是托管的。

所以现在我很困惑。我的异常框架真的 "fixed" 有问题吗?如果是这样,它是什么以及这个简单的步骤如何解决它?

(我使用的是 Visual Studio 2019 和 .NET Framework 4.8)

编辑:只是添加以显示我的 C++/CLI 集成器的析构函数和终结器 class 以澄清下面 Hans Passant 的评论。

// Take ownership of the given unmanaged pointer.
Integrator::Integrator(std::unique_ptr<gs::Integrator> sip) 
    : m_psi(sip.release())
{
}
Integrator::~Integrator()
{
    // Clean up and null out in case we get called twice

    delete m_psi;
    m_psi = nullptr;
}
Integrator::!Integrator()
{
    this->~Integrator();
}
var integrator = Integrator.Create();
_logger.Debug("Integrating normal map.  Res=" + model.ResolutionMmpp);
var hm = integrator.IntegrateNormalMap(nm, model.ResolutionMmpp);

// *** 'integrator' gets finalized before I even get here.  How? ***
_logger.Debug("Back from integrate"); 

integrator' 在我到达这里之前就完成了。怎么样?

此后不再使用变量积分器,因此可以免费进行垃圾回收。这实际上已经是真的,就在它的成员 m_psi 在下面的调用中被访问之后,因为它是对 'this'.

的最后引用
HeightMap^ Integrator::IntegrateNormalMap(NormalMap^ nrm, double res)
{
    //    Note:  'm_psi' is a pointer to the valid, unmanaged C++ object
    return gcnew HeightMap(m_psi->integrateNormalMap(nrm->sdkMap(), res));
}

正如您所说,您在 Integrator 的终结器中删除了 m_psi。 为防止这种情况,您只需防止集成器在您的呼叫完成之前被垃圾收集。例如:将其设为字段或在使用 GC.KeepAlive(integrator);

调用后访问它

关于 .Net 4.8/4.7.2: [C#] 我可以确认,这是 4.8 中新增的 bug/feature。我编写了一个示例应用程序来重现该行为。在 4.7.2 中,对象保持活动状态,在 4.8 中,它在方法调用期间被 GC。 然后,我用纯 C# 重现了相同的行为并得到了相同的结果,只要一个对象不再被引用,它就会被垃圾收集,即使其中一个方法仍然是 运行。对于 C# 这当然没有问题,因为其数据仍可访问的对象将永远不会被垃圾收集。

https://github.com/c-tair/CSharpAgressiveGC