【问题标题】:System.EngineExecutionException when PInvoking native code with callbacks使用回调调用本机代码时出现 System.EngineExecutionException
【发布时间】:2021-03-10 14:38:19
【问题描述】:

我试图找出EngineExecutionException 的根本原因。我已将其范围缩小到我认为是可重现的最小示例。

我有两个项目,一个非托管 C++ DLL 和一个托管 C# 控制台应用程序。非托管代码有两个函数,一个存储回调,另一个调用它:

#define WINEXPORT extern "C" __declspec(dllexport)

typedef bool (* callback_t)(unsigned cmd, void* data);
static callback_t callback;

WINEXPORT void set_callback(callback_t cb)
{
    callback = cb;
}

WINEXPORT void run(void)
{
    callback(123, nullptr);
}

在 C# 方面:

using System;
using System.Runtime.InteropServices;
using System.Threading.Tasks;

namespace ExecutionExceptionReproConsole
{
    class Program
    {
        private const string dllPath = "ExecutionExceptionReproNative.dll";

        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        [return: MarshalAs(UnmanagedType.I1)]
        private delegate bool callback_t(uint cmd, IntPtr data);

        [DllImport(dllPath, CallingConvention = CallingConvention.Cdecl)]
        private static extern void set_callback(callback_t callback);

        [DllImport(dllPath, CallingConvention = CallingConvention.Cdecl)]
        private static extern void run();

        static async Task Main(string[] args)
        {
            set_callback(Callback);
            while (!Console.KeyAvailable)
            {
                run();
                await Task.Delay(1);
            }
        }

        static bool Callback(uint cmd, IntPtr data)
        {
            return true;
        }
    }
}

当我运行控制台应用程序时,它可以正常运行三分半钟,然后在调用 run() 时出现 System.EngineExecutionException 崩溃。

调用栈:

    [Managed to Native Transition]      Annotated Frame
>   ExecutionExceptionReproConsole.dll!ExecutionExceptionReproConsole.Program.Main(string[] args = {string[0x00000000]}) Line 26    C#  Symbols loaded.
    [Resuming Async Method]     Annotated Frame
    System.Private.CoreLib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state)   Unknown No symbols loaded.
    System.Private.CoreLib.dll!System.Runtime.CompilerServices.AsyncTaskMethodBuilder<System.Threading.Tasks.VoidTaskResult>.AsyncStateMachineBox<ExecutionExceptionReproConsole.Program.<Main>d__4>.MoveNext(System.Threading.Thread threadPoolThread) Unknown No symbols loaded.
    System.Private.CoreLib.dll!System.Runtime.CompilerServices.TaskAwaiter.OutputWaitEtwEvents.AnonymousMethod__12_0(System.Action innerContinuation, System.Threading.Tasks.Task innerTask = Id = 0x000036d4, Status = RanToCompletion, Method = "{null}") Unknown No symbols loaded.
    System.Private.CoreLib.dll!System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(System.Action action, bool allowInlining)   Unknown No symbols loaded.
    System.Private.CoreLib.dll!System.Threading.Tasks.Task.RunContinuations(object continuationObject)  Unknown No symbols loaded.
    System.Private.CoreLib.dll!System.Threading.Tasks.Task.TrySetResult()   Unknown No symbols loaded.
    System.Private.CoreLib.dll!System.Threading.Tasks.Task.DelayPromise.CompleteTimedOut()  Unknown No symbols loaded.
    System.Private.CoreLib.dll!System.Threading.TimerQueueTimer.CallCallback(bool isThreadPool) Unknown No symbols loaded.
    System.Private.CoreLib.dll!System.Threading.TimerQueueTimer.Fire(bool isThreadPool) Unknown No symbols loaded.
    System.Private.CoreLib.dll!System.Threading.TimerQueue.FireNextTimers() Unknown No symbols loaded.

什么可能导致崩溃?

其他一些信息:

  • Visual Studio 版本为 16.8.2。
  • 我正在为 x64 构建。 x86 仍然会出现这个问题,但抛出的时间大约是原来的两倍。
  • 我使用的是 .NET 5.0,但我也可以使用 .NET Core 3.1 和 2.1 重现该问题。
    • 尤其是 .NET Core 2.1,它崩溃的速度快得多,大约 20 秒而不是三分半钟。
  • 我注意到内存使用量在应用程序的运行时稳步攀升,但还不足以让它耗尽。它以大约 16 kB/s 的速度攀升,最终在崩溃时达到 13 MB(据诊断工具报告)。
  • 如果我将Task.Delay 时间降低到 0,或者如果我在同步循环而不是异步中运行,我将无法重现该问题。我没有注意到在这些情况下内存使用量会增加。
  • 如果我在 C++ 代码中注释掉来自 run() 的回调调用,我将无法重现该问题。
  • 如果我使用带有LoadLibraryGetProcAddress 而不是DllImportstatic extern ... 的C# 9.0 函数指针,我可以重现该问题。

【问题讨论】:

  • .NET 可以将您传递给set_callback 的回调方法移动到内存中的不同位置。为避免这种情况,请尝试 1) 定义 static readonly callback_t _cb = Callback; 它将自动修复引用,然后 2) 改用 set_callback(_cb)
  • 嗯,在收集的委托对象上崩溃是一种奇怪的方式。当您使用调试器时,您应该首先看到 callbackOnCollectedDelegate 托管调试助手通知,然后在您不使用时看到 AVE。记录 .NET 风格。快点在循环内调用 GC.Collect()。
  • @SimonMourier 谢谢 -- static readonly callback_t ... 避免了崩溃。
  • @HansPassant 对于它的价值,我启用了所有的 MDA。循环内的GC.Collect() 使其立即崩溃。我在 Windows 10 上,它在所有三个 .NET 5.0、.NET Core 3.1 和 .NET Core 2.1 上都崩溃了。
  • 前进一步,后退两步。我会为那个 MDA 烧一根蜡烛,再等一年。

标签: c# c++ pinvoke


【解决方案1】:

正如其他人所指出的,这是由于 .NET 垃圾收集了实际的委托。这是 .NET p/Invoke 的一个常见问题。

具体来说,这段代码:

set_callback(Callback);

这个代码实际上是syntactic sugar

set_callback(new callback_t(Callback));

如您所见,callback_t 实例实际上并未保存在任何地方。所以,set_callback 返回后,它就不再是 root 了,符合 GC 的条件。

最简单的解决方案是将它保存在一个有根变量中,直到它不再被 C++ 代码引用:

static async Task Main(string[] args)
{
    _callback = Callback;
    set_callback(_callback);
    while (!Console.KeyAvailable)
    {
        run();
        GC.Collect();
        await Task.Delay(1);
    }
}

private static callback_t _callback;

请注意,使此同步或将 Task.Delay 更改为 0 将删除最终导致 GC 的 Task 分配,从而释放委托。

【讨论】:

    猜你喜欢
    • 2013-06-09
    • 1970-01-01
    • 2012-04-18
    • 2013-01-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多