无论循环内代码执行时间多长,每隔x秒运行一次代码

5
我正在尝试让一个LED按照某首歌曲的节奏闪烁。这首歌曲的BPM精确为125。
我编写的代码似乎在一开始运行时就能工作,但是它运行时间越长,LED闪烁和下一个节拍开始之间的时间差就越大。 LED似乎比应该闪烁的速度略慢了一点。

我认为这是因为lastBlink有点依赖于刚刚发生的闪烁,而不是使用一个静态的初始值进行同步...

unsigned int bpm = 125;
int flashDuration = 10;
unsigned int lastBlink = 0;
for(;;) {
    if (getTickCount() >= lastBlink+1000/(bpm/60)) {
        lastBlink = getTickCount();
        printf("Blink!\r\n");
        RS232_SendByte(cport_nr, 4); //LED ON
        delay(flashDuration);
        RS232_SendByte(cport_nr, 0); //LED OFF
    }
}

1
请记住,每个函数调用都需要一些时间来执行。如果可以确定时间,您可以将其包含在偏移量中。如果无法确定时间,尽管我很不想这么说,您可能需要尝试多线程方法并在线程之间进行信号传递。(我为自己说出这句话感到难过。) - callyalater
你可以使用操作系统提供的计时器函数。 - M.M
7个回答

3

将值添加到 lastBlink,而不要重新读取它,因为 getTickCount 可能已经跳过了比等待的精确节拍更多的节拍。

lastblink+=1000/(bpm/60);

3
繁忙等待很糟糕,它会无故旋转CPU,在大多数操作系统下,它会导致您的进程受到惩罚——操作系统会注意到它正在使用大量CPU时间,并动态降低其优先级,以便其他不那么贪婪的程序首先获得CPU时间。相比之下,等待指定时间(s)更好。
关键在于根据当前系统时钟时间动态计算睡眠时间,直到下一次闪烁。 (简单地延迟固定时间意味着您最终会漂移,因为每次循环迭代需要一定的非零和有些不确定的时间来执行)。
以下是示例代码(在MacOS / X下测试过,可能也可以在Linux下编译,但可以通过一些更改适应任何操作系统)。
#include <stdio.h>
#include <unistd.h>
#include <sys/times.h>

// unit conversion code, just to make the conversion more obvious and self-documenting
static unsigned long long SecondsToMillis(unsigned long secs) {return secs*1000;}
static unsigned long long MillisToMicros(unsigned long ms)    {return ms*1000;}
static unsigned long long NanosToMillis(unsigned long nanos)  {return nanos/1000000;}

// Returns the current absolute time, in milliseconds, based on the appropriate high-resolution clock
static unsigned long long getCurrentTimeMillis()
{
#if defined(USE_POSIX_MONOTONIC_CLOCK)
   // Nicer New-style version using clock_gettime() and the monotonic clock
   struct timespec ts;
   return (clock_gettime(CLOCK_MONOTONIC, &ts) == 0) ? (SecondsToMillis(ts.tv_sec)+NanosToMillis(ts.tv_nsec)) : 0;
#  else
   // old-school POSIX version using times()
   static clock_t _ticksPerSecond = 0;
   if (_ticksPerSecond <= 0) _ticksPerSecond = sysconf(_SC_CLK_TCK);

   struct tms junk; clock_t newTicks = (clock_t) times(&junk);
   return (_ticksPerSecond > 0) ? (SecondsToMillis((unsigned long long)newTicks)/_ticksPerSecond) : 0;
#endif
}

int main(int, char **)
{
   const unsigned int bpm = 125;
   const unsigned int flashDurationMillis = 10;
   const unsigned int millisBetweenBlinks = SecondsToMillis(60)/bpm;
   printf("Milliseconds between blinks:  %u\n", millisBetweenBlinks);

   unsigned long long nextBlinkTimeMillis = getCurrentTimeMillis();
   for(;;) {
       long long millisToSleepFor = nextBlinkTimeMillis - getCurrentTimeMillis();
       if (millisToSleepFor > 0) usleep(MillisToMicros(millisToSleepFor));

       printf("Blink!\r\n");
       //RS232_SendByte(cport_nr, 4); //LED ON
       usleep(MillisToMicros(flashDurationMillis));
       //RS232_SendByte(cport_nr, 0); //LED OFF
       nextBlinkTimeMillis += millisBetweenBlinks;
   }
}

休眠功能似乎能够工作,但是现在它从来都不准时。总是差几毫秒。你有什么想法吗?http://i.snag.gy/FAeYz.jpg - Forivin
好的,这可能是因为我正在使用Windows的GetTickCount()函数,该函数显然返回非常不准确的结果。 - Forivin
如果我没记错的话,Windows 默认使用 10 毫秒的滴答时间 - 如果你想将其减少到 1 毫秒,可以在 main() 的顶部调用 timeBeginPeriod(1)。 - Jeremy Friesner

3
我认为漂移问题可能源于您使用相对时间延迟,即通过固定的持续时间睡眠,而不是睡眠到绝对时间点。问题在于由于调度问题,线程并不总是精确地按时唤醒。
像这样的解决方案可能适合您:
// for readability
using clock = std::chrono::steady_clock;

unsigned int bpm = 125;
int flashDuration = 10;

// time for entire cycle
clock::duration total_wait = std::chrono::milliseconds(1000 * 60 / bpm);

// time for LED off part of cycle
clock::duration off_wait = std::chrono::milliseconds(1000 - flashDuration);

// time for LED on part of cycle
clock::duration on_wait = total_wait - off_wait;

// when is next change ready?
clock::time_point ready = clock::now();

for(;;)
{
    // wait for time to turn light on
    std::this_thread::sleep_until(ready);

    RS232_SendByte(cport_nr, 4); // LED ON

    // reset timer for off
    ready += on_wait;

    // wait for time to turn light off
    std::this_thread::sleep_until(ready);

    RS232_SendByte(cport_nr, 0); // LED OFF

    // reset timer for on
    ready += off_wait;
}

2
如果您的问题是漂移而不是延迟,我建议从给定的起点开始测量时间,而不是从上一个眨眼开始。
start = now()
blinks = 0
period = 60 / bpm
while true
    if 0 < ((now() - start) - blinks * period)
        ledon()
        sleep(blinklengh)
        ledoff()
        blinks++

哦,if语句到底是做什么的?if (now() - start) - blink * period - Forivin
我错过了一个“0 <”。 它本应该做的事情(现在已经做到了) 是将总时间(现在() - 开始)减去之前闪烁所花费的时间(blink * period)。如果结果大于限制,那么就是另一个闪烁的时候了。 与一开始相比计数的好处是,除非您的时钟不准确或播放速度偏移,否则您永远不应该失去同步。 - nissefors
确实,那就是我想表达的意思。 - nissefors
嗯,似乎不起作用。在每次迭代中,我似乎都进入了if语句,因此LED基本上始终处于开启状态。http://pastebin.com/7eCty0yK - Forivin
1000/(125/60) 的结果是480。所以我应该看到LED闪烁,但是我没有看到。 :/ - Forivin
显示剩余5条评论

1
由于您没有指定C++98/03,我假设至少是C++11,因此<chrono>可用。这与Galik's answer一致。但是,我会设置它,以便更精确地使用<chrono>的转换能力,而无需手动输入转换因子,除了描述“每分钟拍数”或实际上在这个答案中是相反的:“每拍几分钟”。
using namespace std;
using namespace std::chrono;
using mpb = duration<int, ratio_divide<minutes::period, ratio<125>>>;
constexpr auto flashDuration = 10ms;
auto beginBlink = steady_clock::now() + mpb{0};
while (true)
{
    RS232_SendByte(cport_nr, 4); //LED ON
    this_thread::sleep_until(beginBlink + flashDuration);
    RS232_SendByte(cport_nr, 0); //LED OFF
    beginBlink += mpb{1};
    this_thread::sleep_until(beginBlink);
}

首先要做的是指定一拍的持续时间,即“分钟/125”。这就是mpb的作用。我使用minutes::period作为60的替代,只是为了提高可读性和减少魔法数字的数量。
假设使用C++14,我可以给flashDuration设置实际单位(毫秒)。在C++11中,这需要使用更冗长的语法来拼写:
constexpr auto flashDuration = milliseconds{10};

然后是循环:这个设计与 Galik's answer非常相似,但我每次迭代只增加时间以启动闪烁,每次增加的时间为60/125秒。通过延迟到指定的time_point,而不是指定的duration,可以确保随着时间的推移没有舍入积累。通过使用完全描述所需持续时间间隔的单位,也不存在计算下一个间隔的起始时间的舍入误差。无需处理毫秒。也不需要计算需要延迟多长时间。只需要符号地计算每次迭代的开始时间。抱歉挑剔 Galik's answer,我认为它是除了我的答案之外第二好的答案,但它展示了一个错误,而我的答案不仅没有这个错误,而且还设计了防止这个错误。我直到用计算器挖掘它时才注意到它,它足够微妙,以至于测试可能会忽略它。
在Galik的回答中,包含以下HTML标签:


total_wait =  480ms;  // this is exactly correct
off_wait   =  990ms;  // likely a design flaw
on_wait    = -510ms;  // certainly a mistake

一个迭代所需的总时间是 on_wait + off_wait,即 440ms,几乎与 total_wait (480ms) 没有区别,这使得调试非常具有挑战性。

相比之下,我的答案只增加了一次 ready (beginBlink),并且恰好为 480ms

我的答案更可能是正确的,因为它将更多的计算委托给了 <chrono> 库。在这种特殊情况下,这种概率得到了回报。

避免手动转换。相反,让 <chrono> 库为您执行转换。手动转换会引入错误的可能性。


0
你需要计算进程所花费的时间并将其从flashDuration值中减去。

0
最明显的问题是当你将bpm除以60时,会丢失精度。这总是得到一个整数(2),而不是2.08333333...。
调用getTickCount()两次也可能导致一些漂移。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接