【问题标题】:Threading and timers inside an emulator模拟器中的线程和定时器
【发布时间】:2013-03-23 22:02:49
【问题描述】:

我正在用 C# 开发一个 Chip-8 仿真器,我已经完成了几乎所有方面的部分工作,但我仍然对仿真器的速度控制感到疑惑。

我现在正在做的是假设我每秒获得 60 帧,我使用一个以以下方式触发 1/60 秒的计时器(伪代码):

timer_ticked()
{
    for(int i = 0; i < Settings.CyclesPerFrame; i++)
    {
        EmulateCycle();
    }

    if (IsDrawFlagSet)
    {
        DrawGraphics();
    }
}

我正在使用一个名为 microtimer http://www.codeproject.com/Articles/98346/Microsecond-and-Millisecond-NET-Timer 的高分辨率计时器,我相信计时器不会等待 timer_ticked 完成来触发下一个周期(例如,创建一个新线程)并且我有一个线程和表单的问题,因为尝试使用 GDI 绘制到窗口(使用 control.GetGraphics() 方法)似乎是线程安全的,但尝试创建 SDLDotNet 方法(只是一个示例)却不是。

您认为哪种方法可以最好地控制模拟器的速度而不会陷入计时器线程的疯狂?

PS:模拟器的源码可以在GitHub上找到:https://github.com/AlFranco/C8POC

谢谢!

【问题讨论】:

  • 伪代码不能保证每秒 60 帧,因为您受制于操作系统和处理器本身。如果GetGraphics()实际上是线程安全的,我会非常震惊。
  • 感谢您的回答!所以 16,6ms 对于普通计时器来说太精确了?你知道更好的解决方案吗?

标签: c# .net multithreading gdi emulation


【解决方案1】:

如果您勾选方法在完成之前再次被调用,则问题不在于您的计时器。这是因为处理时间超过了 16.6 毫秒。获得更好的计时器并不能解决您的问题。

也就是说,您可以通过多种方式防止重入。

您可以在输入回调时禁用计时器,并在完成后重新启用它。这将防止多次调用:

timer_ticked()
{
    timer.Enabled = false;
    // do stuff here
    timer.Enabled = true;
}

请注意,这并不能为您提供完美的 16.6 毫秒滴答频率。相反,下一个滴答声将在您启用计时器后 16.6 毫秒(大约)发生。您的实际周期是 16.6 毫秒,加上处理所需的时间。

唯一会失败的情况是在下一次滴答发生之前没有调用 timer_ticked 方法。

如果您想保证不能获得并发滴答,可以使用System.Threading.Timer 并将其设置为一次性(无周期性信号)。例如:

Timer myTimer = new Timer(timer_tick, null, 16, -1);

最后一个参数中的-1 告诉它不是周期性定时器。它会触发一次并停止。

然后,在你的计时器滴答声中:

timer_tick()
{
    // do stuff
    // restart the timer
    myTimer.Change(16, -1);
}

编辑

如果处理程序仍在处理上一个滴答声,您不能轻易告诉计时器不要发出滴答声。但是,您可以阻止计时器滴答处理程序在后续滴答中执行任何操作。您只需使用Monitor

private object timerLock = new object();
timer_ticked()
{
    if (!Monitor.TryEnter(timerLock))
        return;
    try
    {
        // do stuff here
    }
    finally
    {
        Monitor.Exit(timerLock);
    }
}

这种解决方案的问题在于,如果您的计时器设置为 16 毫秒并且处理程序需要 17 毫秒,那么您的有效更新率将是每 32 毫秒一次,因为第二个滴答实际上被忽略了。你最好选择一次性计时器交易。

另一种可能性是使用Stopwatch 来计算处理程序花费的时间,然后从下一个延迟时间中减去:

timer_ticked()
{
    var sw = Stopwatch.StartNew();
    // do stuff
    timer.Change(16-sw.ElapsedMilliseconds, -1);
}

但这并不是那么简单。如果处理程序完成它的时间超过 16 毫秒,那么您最终会得到一个负延迟。所以你会想要:

var delayTime = Math.Max(0, 16 - sw.ElapsedMilliseconds);
timer.Change(delayTime, -1);

但是,如果您的处理程序经常花费比计时器延迟更长的时间,那么这对您没有帮助。您要么必须降低计时器频率(即延长延迟时间),要么优化处理代码。

【讨论】:

  • 我明白你的意思。在仿真中是否有更好的方法来模拟处理器的滴答声?根据我的想法,我剩下的唯一想法是实现一个计时器,该计时器仅在前一个滴答函数完成时每 x 毫秒触发一次,并在等待时立即触发。同样,我不受上述方法的束缚,如果您想到任何方法,我认为讨论其他替代方案会很好:)。谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-11-16
  • 2011-05-18
  • 1970-01-01
  • 2021-12-03
相关资源
最近更新 更多