C#函数指针从C++调用时触发访问冲突问题排查
函数指针替代委托与C++交互时偶发访问冲突问题排查
问题背景
为提升跨语言交互性能,在C#中尝试用函数指针替代委托类型与C++模块交互。委托封送版本运行稳定无异常,但改用函数指针后,偶发触发0xC0000005访问冲突错误。
代码片段
C# 代码
public static class Engine { // 偶发访问冲突的函数指针版本 [DllImport("engine", CallingConvention = CallingConvention.Cdecl)] public static unsafe extern UInt32 EventScheduler_ScheduleLocalEvent( int priority, UInt64 delay, delegate* unmanaged[Cdecl]<nint, void> callback, nint obj); // 运行正常但性能较差的委托版本 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void EventScheduler_ScheduleLocalEvent_callbackDelegate(nint obj); [DllImport("engine", CallingConvention = CallingConvention.Cdecl)] public static unsafe extern UInt32 EventScheduler_ScheduleLocalEvent( int priority, UInt64 delay, [MarshalAs(UnmanagedType.FunctionPtr)] EventScheduler_ScheduleLocalEvent_callbackDelegate callback, nint obj); } public static class EventScheduler { public delegate void EventCallback(); // 函数指针版本专用回调函数 [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) }, EntryPoint = "ScheduleLocalEvent_EventCallbackFunction")] private static void ScheduleLocalEvent_EventCallbackFunction(nint handlePtr) { Thread.BeginThreadAffinity(); GCHandle handle = GCHandle.FromIntPtr(handlePtr); var cb = (EventScheduler.EventCallback)handle.Target!; cb(); handle.Free(); Thread.EndThreadAffinity(); } public static UInt32 ScheduleLocalEvent(int priority, SimulationTime delayTime, EventCallback eventCallback) { GCHandle handle = GCHandle.Alloc(eventCallback, GCHandleType.Normal); IntPtr handlePtr = GCHandle.ToIntPtr(handle); unsafe { return Engine.EventScheduler_ScheduleLocalEvent( priority, delayTime, &ScheduleLocalEvent_EventCallbackFunction, handlePtr); } } }
C++ 代码
#define API extern "C" __declspec(dllexport) API uint32_t EventScheduler_ScheduleLocalEvent(int priority, uint64_t delay, void(__cdecl* callback)(void* obj), void* obj) { ERSAssert(Ers::Core::InsideSubModel()); Core::SyncManagerBase& syncManager = Ers::Core::GetSyncManagerBase(); return syncManager.ScheduleEvent(priority, delay, [callback, obj]() { callback(obj); }); }
错误信息
运行时输出:
AWealthOfRows.exe (process 39016) exited with code -1073741819.
详细调试日志:
'AWealthOfRows.exe' (Win32): Loaded ...(省略部分加载信息) Exception thrown at 0x000001F303570860 in AWealthOfRows.exe: 0xC0000005: Access violation reading location 0x000001F30385FB32. The Common Language Runtime cannot stop at this exception. Common causes include: incorrect COM interop marshalling and memory corruption. To investigate further use native-only debugging. ...(省略后续线程退出信息) The program '[39016] AWealthOfRows.exe' has exited with code 3221225477 (0xc0000005) 'Access violation'.
调试发现:异常触发在C++调用回调函数、进入C#回调作用域之前。曾尝试切换调用约定(cdecl/stdcall),未解决问题。
排查假设与已知条件
假设
- 标记
[UnmanagedCallersOnly]的静态函数地址稳定,尤其指定EntryPoint后 Thread.BeginThreadAffinity()/Thread.EndThreadAffinity()可保证非托管线程正常执行托管代码
已知条件
- 调用该函数的线程为非C#创建的外部线程,且线程数量较多
问题原因分析
核心问题在于GCHandle生命周期管理错误:
- 委托版本中,CLR会自动维护委托的引用计数,确保非托管端不再使用前不会被GC回收;而函数指针版本完全依赖手动管理GCHandle。
- 当前代码在回调执行完成后立即调用
handle.Free(),若C++端存在重复调度同一事件(如逻辑错误导致回调被多次触发),第二次调用时会访问已释放的GCHandle内存,触发访问冲突。 - 线程亲和性并非问题根源,
Thread.BeginThreadAffinity()仅用于绑定托管代码到当前OS线程,与内存访问错误无关。
解决方案
1. 修正GCHandle释放时机
确保GCHandle在回调所有可能的调用完成后再释放,而非回调内部立即释放:
- 修改C#回调函数,移除内部的
handle.Free():[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] private static void ScheduleLocalEvent_EventCallbackFunction(nint handlePtr) { Thread.BeginThreadAffinity(); GCHandle handle = GCHandle.FromIntPtr(handlePtr); var cb = (EventScheduler.EventCallback)handle.Target!; try { cb(); } finally { Thread.EndThreadAffinity(); } } - 添加专门的GCHandle释放函数,让C++端在事件彻底完成后调用:
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] private static void ReleaseEventCallbackHandle(nint handlePtr) { GCHandle handle = GCHandle.FromIntPtr(handlePtr); handle.Free(); } - 修改C++导出函数,增加释放回调参数,并在事件完成后调用:
API uint32_t EventScheduler_ScheduleLocalEvent(int priority, uint64_t delay, void(__cdecl* callback)(void* obj), void(__cdecl* releaseHandle)(void* obj), void* obj) { ERSAssert(Ers::Core::InsideSubModel()); Core::SyncManagerBase& syncManager = Ers::Core::GetSyncManagerBase(); return syncManager.ScheduleEvent(priority, delay, [callback, releaseHandle, obj]() { callback(obj); releaseHandle(obj); // 事件完成后释放GCHandle }); } - 更新C#调用逻辑,传递释放函数指针:
private static unsafe delegate* unmanaged[Cdecl]<nint, void> _releasePtr = &ReleaseEventCallbackHandle; private static unsafe delegate* unmanaged[Cdecl]<nint, void> _callbackPtr = &ScheduleLocalEvent_EventCallbackFunction; public static UInt32 ScheduleLocalEvent(int priority, SimulationTime delayTime, EventCallback eventCallback) { GCHandle handle = GCHandle.Alloc(eventCallback, GCHandleType.Normal); IntPtr handlePtr = GCHandle.ToIntPtr(handle); unsafe { return Engine.EventScheduler_ScheduleLocalEvent( priority, delayTime, _callbackPtr, _releasePtr, handlePtr); } }
2. 验证C++事件调度逻辑
排查C++端SyncManagerBase::ScheduleEvent是否存在重复触发同一事件的逻辑错误,确保每个事件仅被回调一次。
3. 缓存函数指针地址
虽然[UnmanagedCallersOnly]静态函数地址理论上稳定,但可提前缓存指针地址,避免极端情况下的地址变化:
private static unsafe delegate* unmanaged[Cdecl]<nint, void> _callbackPtr = &ScheduleLocalEvent_EventCallbackFunction;
总结
函数指针版本的性能优势建立在手动内存管理的基础上,必须严格保证GCHandle的存活时间覆盖回调的所有调用场景。委托版本的稳定性源于CLR自动管理引用计数,而函数指针版本需要开发者完全掌控资源生命周期。
内容的提问来源于stack exchange,提问作者Mark Oostveen
相关产品推荐
相关产品推荐

