【发布时间】: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()的回调调用,我将无法重现该问题。 - 如果我使用带有
LoadLibrary和GetProcAddress而不是DllImport和static 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 烧一根蜡烛,再等一年。