System.EngineExecutionException 当 PInvoking native code with callbacks 时
System.EngineExecutionException when PInvoking native code with callbacks
我正在尝试找出 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
和 [=22 的 C# 9.0 函数指针,我 可以 重现该问题=].
正如其他人所指出的,这是由于 .NET 垃圾收集了实际的委托。这是 .NET 的一个常见问题 p/Invoke。
具体来说,这段代码:
set_callback(Callback);
实际上是 syntactic sugar 这个代码:
set_callback(new callback_t(Callback));
如您所见,callback_t
实例实际上并未保存在任何地方。所以,在set_callback
returns之后,就不再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
分配,释放委托。
我正在尝试找出 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
和 [=22 的 C# 9.0 函数指针,我 可以 重现该问题=].
正如其他人所指出的,这是由于 .NET 垃圾收集了实际的委托。这是 .NET 的一个常见问题 p/Invoke。
具体来说,这段代码:
set_callback(Callback);
实际上是 syntactic sugar 这个代码:
set_callback(new callback_t(Callback));
如您所见,callback_t
实例实际上并未保存在任何地方。所以,在set_callback
returns之后,就不再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
分配,释放委托。