You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用WinMM.dll实现高精度定时器如何避免GC回收引发的崩溃?

崩溃根因

你遇到的崩溃不是GC阻塞定时器导致的,核心原因是传递给WinMM非托管接口的回调委托被GC回收了:
你在StartElapsedTimer方法中创建的TimerEventHandler委托是局部变量,方法执行完成后没有任何托管引用指向该委托,GC运行时会直接将其标记为可回收对象,而WinMM的非托管逻辑还持有该委托的指针,触发回调时就会抛出CallbackOnCollectedDelegate错误。
另外你传入的myData是方法栈上的局部变量,方法执行完成后栈内存会被释放,非托管代码访问该地址也会引发未知异常。

修复方案

只需要延长回调委托和用户上下文变量的生命周期,和WinMMWrapper实例保持一致即可,修改步骤如下:

  1. 在WinMMWrapper类中新增两个私有字段,存储回调委托和用户上下文
  2. 构造函数中提前初始化回调委托
  3. 修改StartElapsedTimer方法,使用预存的字段作为参数传入非托管接口

修改后的核心代码

public class WinMMWrapper : IDisposable
{
    [DllImport("WinMM.dll", SetLastError = true)]
    public static extern uint timeSetEvent(int msDelay, int msResolution,
        TimerEventHandler handler, ref int userCtx, int eventType);

    [DllImport("Winmm.dll", CharSet = CharSet.Auto)]
    static extern uint timeKillEvent(uint uTimerID);

    public delegate void TimerEventHandler(uint id, uint msg, ref int userCtx,
        int rsv1, int rsv2);

    public enum TimerEventType
    {
        OneTime = 0,
        Repeating = 1,
    }

    private readonly Action _elapsedAction;
    private readonly int _elapsedMs;
    private readonly int _resolutionMs;
    private readonly TimerEventType _timerEventType;
    private uint _timerId;
    private bool _disposed;
    // 新增字段:存储回调委托,生命周期和实例一致
    private TimerEventHandler _tickHandler;
    // 新增字段:存储用户上下文,避免栈内存释放
    private int _userCtx;

    public WinMMWrapper(int elapsedMs, int resolutionMs, TimerEventType timerEventType, Action elapsedAction)
    {
        _elapsedMs = elapsedMs;
        _resolutionMs = resolutionMs;
        _timerEventType = timerEventType;
        _elapsedAction = elapsedAction;
        // 提前初始化委托,存在实例字段中不会被GC回收
        _tickHandler = TickHandler;
        _userCtx = 1;
    }

    public bool StartElapsedTimer()
    {
        StopTimer();

        // 直接传入预存的委托和用户上下文字段,不要创建局部委托
        _timerId = timeSetEvent(_elapsedMs, _resolutionMs / 10, _tickHandler, ref _userCtx, (int)_timerEventType);
        return _timerId != 0;
    }

    // 其余代码保持不变
    public void StopTimer()
    {
        if (_timerId != 0)
        {
            timeKillEvent(_timerId);
            _timerId = 0;
        }
    }

    private void TickHandler(uint id, uint msg, ref int userctx, int rsv1, int rsv2)
    {
        _elapsedAction();
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private void Dispose(bool disposing)
    {
        if (!_disposed && disposing)
            StopTimer();

        _disposed = true;
    }

    ~WinMMWrapper()
    {
        Dispose(false);
    }
}
注意事项
  • 你的WinMMWrapper实例存储在静态变量Global.timer中,静态变量属于GC根引用,只要不主动置为null,实例和其持有的委托字段就不会被回收,完全满足定时器长期运行的要求
  • 不需要使用定时器时,要主动调用Global.timer.Dispose()方法释放非托管的定时器资源,避免内存泄漏

内容的提问来源于stack exchange,提问作者Ozgur Saklanmaz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.01 00:18:04