Skip to content

Latest commit

 

History

History
587 lines (405 loc) · 22.5 KB

File metadata and controls

587 lines (405 loc) · 22.5 KB

十一、时间点和间隔

嵌入式应用处理物理世界中发生的事件和控制过程-这就是正确处理时间和延迟对它们至关重要的原因。 切换红绿灯;产生声音音调;来自多个传感器的数据同步-所有这些任务都依赖于适当的时间测量。

普通 C 没有提供任何处理时间的标准函数。 预计应用开发人员将使用特定于目标操作系统(Windows、Linux 或 MacOS)的 Time API。 对于裸机嵌入式系统,开发人员必须基于特定于目标平台的低级计时器 API 创建自定义函数来处理时间。 因此,代码很难移植到其他平台。

为了克服可移植性问题,C++(从 C++ 11 开始)定义了处理时间和时间间隔的数据类型和函数。 此 API 称为std::chrono库,可帮助开发人员在任何环境和任何目标平台上以统一的方式使用时间。

在本章中,我们将学习如何在应用中使用时间戳、时间间隔和延迟。 我们将讨论一些与时间管理相关的常见陷阱,以及它们的适当解决方法。

我们将介绍以下主题:

  • 探索 C++ 计时库
  • 测量时间间隔
  • 在延迟的情况下工作
  • 使用单调时钟
  • 使用可移植操作系统接口(POSIX)时间戳

使用这些方法,您将能够编写在任何嵌入式平台上工作的时间处理的可移植代码。

探索 C++ 计时库

从 C++ 11 开始,C++ Chrono 库提供标准化的数据类型和函数来处理时钟、时间点和时间间隔。 在本食谱中,我们将探索 Chrono 库的基本功能,并学习如何处理时间点和时间间隔。

我们还将学习如何使用 C++ 文字来表示更具可读性的时间间隔。

怎么做……

我们将创建一个简单的应用来创建三个时间点,并将它们相互比较。

  1. 在您的~/test工作目录中,创建一个名为chrono的子目录。
  2. 使用您喜欢的文本编辑器在chrono子目录中创建chrono.cpp文件。
  3. 将以下代码片段放入文件中:
#include <iostream>
#include <chrono>

using namespace std::chrono_literals;

int main() {
  auto a = std::chrono::system_clock::now();
  auto b = a + 1s;
  auto c = a + 200ms;

  std::cout << "a < b ? " << (a < b ? "yes" : "no") << std::endl;
  std::cout << "a < c ? " << (a < c ? "yes" : "no") << std::endl;
  std::cout << "b < c ? " << (b < c ? "yes" : "no") << std::endl;

  return 0;
}
  1. 创建包含我们程序的构建规则的CMakeLists.txt文件:
cmake_minimum_required(VERSION 3.5.1)
project(chrono)
add_executable(chrono chrono.cpp)

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

SET(CMAKE_CXX_FLAGS "--std=c++ 14")
set(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)

现在您可以构建和运行应用了。

它是如何运作的..。

我们的应用创建了三个不同的时间点。 第一个是使用系统时钟的now函数创建的:

auto a = std::chrono::system_clock::now();

通过添加1秒和200毫秒的固定时间间隔,从第一个派生出另外两个:

auto b = a + 1s;
auto c = a + 200ms;

请注意我们是如何指定数值旁边的时间单位的。 我们使用了一种名为 C++ 文字的功能。 Chrono 库为基本时间单位定义这样的文字。 要使用这些定义,我们添加了以下内容:

using namespace std::chrono_literals;

这是在我们的main函数之前添加的。

接下来,我们将这些时间点相互比较:

std::cout << "a < b ? " << (a < b ? "yes" : "no") << std::endl;
std::cout << "a < c ? " << (a < c ? "yes" : "no") << std::endl;
std::cout << "b < c ? " << (b < c ? "yes" : "no") << std::endl;

当我们运行应用时,我们看到以下输出:

正如预期的那样,时间点a早于bc,其中时间点c(即a+200 毫秒)早于b(a+1 秒)。 字符串有助于编写更具可读性的代码,C++ Chrono 提供了一组丰富的函数来处理时间。 我们将在接下来的食谱中学习如何使用它们。

还有更多的..。

有关计时库中定义的所有数据类型、模板和函数的信息,请参阅位于https://en.cppreference.com/w/cpp/chrono的计时参考

测量时间间隔

每个与外部硬件交互或响应外部事件的嵌入式应用都必须处理超时和反应时间。 要正确做到这一点,开发人员需要能够以足够的精度测量时间间隔。

C++ Chrono 库提供了一个std::chrono::duration模板化的类,用于处理任意跨度和精度的持续时间。 在本食谱中,我们将学习如何使用该类测量两个时间戳之间的时间间隔,并对照参考持续时间进行检查。

怎么做……

我们的应用将测量简单控制台输出的持续时间,并将其与循环中以前的值进行比较。

  1. 在您的~/test工作目录中,创建一个名为intervals的子目录。
  2. 使用您喜欢的文本编辑器在intervals子目录中创建一个intervals.cpp文件。
  3. 将以下代码片段复制到intervals.cpp文件中:
#include <iostream>
#include <chrono>

int main() {
  std::chrono::duration<double, std::micro> prev;
  for (int i = 0; i < 10; i++) {
    auto start = std::chrono::steady_clock::now();
    std::cout << i << ": ";
    auto end = std::chrono::steady_clock::now();
    std::chrono::duration<double, std::micro> delta = end - start;
    std::cout << "output duration is " << delta.count() <<" us";
    if (i) {
      auto diff = (delta - prev).count();
      if (diff >= 0) {
        std::cout << ", " << diff << " us slower";
      } else {
        std::cout << ", " << -diff << " us faster";
      }
    }
    std::cout << std::endl;
    prev = delta;
  }
  return 0;
}
  1. 最后,创建包含我们程序的构建规则的CMakeLists.txt文件:
cmake_minimum_required(VERSION 3.5.1)
project(interval)
add_executable(interval interval.cpp)

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

SET(CMAKE_CXX_FLAGS "--std=c++ 11")
set(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)

现在您可以构建和运行应用了。

它是如何运作的..。

在应用循环的每次迭代中,我们测量一个输出操作的性能。 为此,我们在操作之前捕获一个时间戳,并在操作完成后捕获另一个时间戳:

 auto start = std::chrono::steady_clock::now();
    std::cout << i << ": ";
 auto end = std::chrono::steady_clock::now();

我们使用 C++ 11auto让编译器推断时间戳的数据类型。 现在,我们需要计算这些时间戳之间的时间间隔。 用一个时间戳减去另一个时间戳就行了。 我们将结果变量显式定义为跟踪double值中的微秒的std::chrono::duration类:

 std::chrono::duration<double, std::micro> delta = end - start;

我们使用另一个相同类型的duration变量来保存前一个值。 在除第一个迭代之外的每个迭代中,我们计算这两个持续时间之间的差异:

    auto diff = (delta - prev).count();

每次迭代时,持续时间和差值都会打印到终端。 当我们运行应用时,我们得到以下输出:

正如我们所看到的,现代 C++ 提供了在应用中处理时间间隔的方便方法。 多亏了重载运算符,可以很容易地获得两个时间点之间的持续时间,并可以对持续时间进行加、减或比较。

还有更多的..。

从 C++ 20 开始,Chrono 库支持将持续时间直接写入输出流,并从输入流中解析持续时间。 不需要显式地将持续时间序列化为整数值或浮点值。 这使得处理持续时间对于 C++ 开发人员来说更加方便。

在延迟的情况下工作

周期性数据处理是许多嵌入式应用中的一种常见模式。 代码不需要一直工作。 如果我们提前知道何时需要处理,应用或工作线程可能在大部分时间处于非活动状态,只有在需要时才会唤醒并处理数据。 它可以节省功耗,或者让设备上运行的其他应用在应用空闲时使用 CPU 资源。

有几种技术可以组织定期处理。 运行带有延迟的循环的工作线程是其中最简单、最常见的一种。

C++ 提供了向当前执行线程添加延迟的标准函数。 在本食谱中,我们将学习两种将延迟添加到应用中的方法,并讨论它们的优缺点。

怎么做……

我们将创建一个具有两个处理循环的应用。 这些循环使用不同的函数来暂停当前线程的执行。

  1. 在您的~/test工作目录中,创建一个名为delays的子目录。
  2. 使用您喜欢的文本编辑器在delays子目录中创建delays.cpp文件。
  3. 让我们首先添加第一个函数sleep_for,以及必要的包含内容:
#include <iostream>
#include <chrono>
#include <thread>

using namespace std::chrono_literals;

void sleep_for(int count, auto delay) {
  for (int i = 0; i < count; i++) {
    auto start = std::chrono::system_clock::now();
    std::this_thread::sleep_for(delay);
    auto end = std::chrono::system_clock::now();
    std::chrono::duration<double, std::milli> delta = end - start;
    std::cout << "Sleep for: " << delta.count() << std::endl;
  }
}
  1. 后跟第二个函数sleep_until
void sleep_until(int count, 
                 std::chrono::milliseconds delay) {
  auto wake_up = std::chrono::system_clock::now();
  for (int i = 0; i < 10; i++) {
    wake_up += delay;
    auto start = std::chrono::system_clock::now();
    std::this_thread::sleep_until(wake_up);
    auto end = std::chrono::system_clock::now();
    std::chrono::duration<double, std::milli> delta = end - start;
    std::cout << "Sleep until: " << delta.count() << std::endl;
  }
}
  1. 接下来,添加一个简单的main函数来调用它们:
int main() {
  sleep_for(10, 100ms);
  sleep_until(10, 100ms);
  return 0;
}
  1. 最后,创建一个CMakeLists.txt文件,其中包含程序的构建规则:
cmake_minimum_required(VERSION 3.5.1)
project(delays)
add_executable(delays delays.cpp)

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

SET(CMAKE_CXX_FLAGS "--std=c++ 14")
set(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)

现在您可以构建和运行应用了。

它是如何运作的..。

在我们的应用中,我们创建了两个函数sleep_forsleep_until。 它们几乎相同,除了sleep_for使用std::this_thread::sleep_for添加延迟,而sleep_until使用std::this_thread::sleep_until

让我们仔细看看sleep_for函数。 它有两个参数-countdelay。 第一个参数定义循环中的迭代次数,第二个参数指定延迟。 我们使用auto作为delay参数的数据类型,让 C++ 为我们推断实际的数据类型。

函数体由单个循环组成:

  for (int i = 0; i < count; i++) {

在每次迭代中,我们运行delay,并通过在delay之前和之后获取时间戳来测量其实际持续时间。 std::this_thread::sleep_for函数接受时间间隔作为参数:

    auto start = std::chrono::system_clock::now();
    std::this_thread::sleep_for(delay);
    auto end = std::chrono::system_clock::now();

实际延迟以毫秒为单位,我们使用double值作为毫秒计数器:

std::chrono::duration<double, std::milli> delta = end - start;

wait_until函数仅略有不同。 它使用std::current_thred::wait_until函数,该函数接受一个时间点来唤醒,而不是一个时间间隔。 我们引入一个额外的wake_up变量来跟踪唤醒时间点:

auto wake_up = std::chrono::system_clock::now();

最初,它被设置为当前时间,并且在每次迭代时,它将作为函数参数传递的延迟加到它的值中:

wake_up += delay;

该函数的其余部分与sleep_for实现相同,但delay函数除外:

std::this_thread::sleep_until(wake_up);

我们运行这两个函数,使用相同的迭代次数和相同的延迟。 请注意,我们如何使用 C++ 字符串向函数传递毫秒数,以提高代码的可读性。 要使用字符串文字,我们添加了以下内容:

sleep_for(10, 100ms);
sleep_until(10, 100ms);

这是在函数定义之上完成的,如下所示:

using namespace std::chrono_literals;

不同的延迟功能会有什么不同吗? 毕竟,我们在两种实现中都使用相同的延迟。 让我们运行代码并比较结果:

有趣的是,我们可以看到sleep_for的所有实际延迟都大于100毫秒,而sleep_until的一些结果低于此值。 我们的第一个函数delay_for没有考虑将数据打印到控制台所需的时间。 sleep_for当您确切知道需要等待多长时间时,sleep_for是一个很好的选择。 然而,如果您的目标是以特定的周期醒来,sleep_until可能是更好的选择。

还有更多的..。

sleep_forsleep_until之间还有其他细微的区别。 系统计时器通常不太精确,可能会通过时间同步服务(如网络时间协议****守护进程(ntpd)进行调整。 这些时钟调整不会影响sleep_for,但sleep_until会将其考虑在内。 如果您的应用依赖于特定时间而不是时间间隔,则使用它;例如,如果您需要每秒在时钟显示上重新绘制数字。

使用单调时钟

C++ 计时库提供三种类型的时钟:

  • 系统时钟
  • 稳定时钟
  • 高分辨率时钟

高分辨率时钟通常被实现为系统时钟或稳定时钟的别名。 然而,系统时钟和稳定时钟有很大的不同。

系统时钟反映系统时间,因此不是单调的。 它可以随时通过网络时间协议(NTP)等时间同步服务进行调整,因此甚至可以倒退。

这使得系统时钟不适合处理精确的持续时间。 稳定的时钟是单调的,它永远不会调整,也永远不会倒退。 此属性有其成本-它与挂钟时间无关,通常表示为自上次重新启动以来的时间。

稳定时钟不应用于需要在重新启动后保持有效的持久时间戳-例如,序列化到文件中或保存到数据库中。 此外,稳定时钟不应用于任何涉及来自不同来源(如远程系统或外部设备)的时间的时间计算。

在本食谱中,我们将学习如何使用稳定时钟来实现一个简单的软件监视器。 运行后台工作线程时,一定要知道它是工作正常还是因为编码错误或外部设备无响应而挂起。 线程定期更新时间戳,而监视例程将时间戳与当前时间进行比较,如果超过阈值,则执行特定的恢复操作。

怎么做……

在我们的应用中,我们将创建一个在后台运行的简单迭代函数,以及在主线程中运行的监视循环。

  1. 在您的~/test工作目录中,创建一个名为monotonic的子目录。
  2. 使用您喜欢的文本编辑器在monotonic子目录中创建monotonic.cpp文件。
  3. 让我们添加标题并定义例程使用的全局变量:
#include <iostream>
#include <chrono>
#include <atomic>
#include <mutex>
#include <thread>

auto touched = std::chrono::steady_clock::now();
std::mutex m;
std::atomic_bool ready{ false };
  1. 后跟后台工作线程例程的代码:
void Worker() {
  for (int i = 0; i < 10; i++) {
    std::this_thread::sleep_for(
         std::chrono::milliseconds(100 + (i % 4) * 10));
    std::cout << "Step " << i << std::endl;
    {
      std::lock_guard<std::mutex> l(m);
      touched = std::chrono::steady_clock::now();
    }
  }
  ready = true;
}
  1. 添加包含监控例程的main函数:
int main() {
  std::thread t(Worker);
  std::chrono::milliseconds threshold(120);
  while(!ready) {
    auto now = std::chrono::steady_clock::now();
    std::chrono::milliseconds delta;
    {
      std::lock_guard<std::mutex> l(m);
      auto delta = now - touched;
      if (delta > threshold) {
        std::cout << "Execution threshold exceeded" << std::endl;
      }
    }
    std::this_thread::sleep_for(std::chrono::milliseconds(10));

  }
  t.join();
  return 0;
}
  1. 最后,创建包含我们程序的构建规则的CMakeLists.txt文件:
cmake_minimum_required(VERSION 3.5.1)
project(monotonic)
add_executable(monotonic monotonic.cpp)
target_link_libraries(monotonic pthread)

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

SET(CMAKE_CXX_FLAGS "--std=c++ 11")
set(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)

现在您可以构建和运行应用了。

它是如何运作的..。

我们的应用是多线程的-它由运行监视的主线程和后台工作线程组成。 我们使用三个全局变量进行同步。

touched变量保存由Worker线程定期更新的时间戳。 由于时间戳由两个线程访问,因此需要保护访问。 为此,我们使用m互斥锁。 最后,为了指示工作线程已经完成了它的工作,使用了一个原子变量ready

工作线程是一个内部包含人为延迟的循环。 延迟是根据步数计算的,导致的延迟从 100 毫秒到 130 毫秒:

std::this_thread::sleep_for(
         std::chrono::milliseconds(100 + (i % 4) * 10));

在每次迭代中,Worker线程更新时间戳。 锁保护用于同步对时间戳的访问:

    {
      std::lock_guard<std::mutex> l(m);
      touched = std::chrono::steady_clock::now();
    }

监控例程在Worker线程运行时循环运行。 在每次迭代中,它计算当前时间和上次更新之间的时间间隔:

      std::lock_guard<std::mutex> l(m);
      auto delta = now - touched;

如果大于阈值,该函数会打印一条警告消息,如下所示:

      if (delta > threshold) {
        std::cout << "Execution threshold exceeded" << std::endl;
      }

在许多情况下,应用可以调用恢复功能来重置外部设备或重新启动线程。 我们在监控循环中添加10毫秒的延迟:

    std::this_thread::sleep_for(std::chrono::milliseconds(10));

这有助于我们减少资源消耗,同时实现可接受的反应时间。 运行应用会产生以下输出:

我们可以在输出中看到几个警告,表明worker线程中的某些迭代花费的时间超过了阈值120毫秒。 这是可以预测的,因为worker函数是这样编写的。 重要的是,我们使用单调的std::chrono::steady_clock函数进行监控。 使用系统时钟可能会导致在时钟调整期间错误地调用恢复功能。

还有更多的..。

C++ 20 定义了几种其他类型的时钟,例如gps_clock,表示全球定位系统(GPS)时间,或file_clock,用于处理文件时间戳。 这些时钟可能是稳定的,也可能不是稳定的或单调的。 使用is_steady成员函数检查时钟是否单调。

使用 POSIX 时间戳

在基于 Unix 的操作系统中,POSIX 时间戳是时间的传统内部表示形式。 POSIX 时间戳定义为自纪元或协调世界时间(UTC),1970 年 1 月 1 日以来的秒数。

由于其简单性,这种表示被广泛用于网络协议、文件元数据或序列化。

在本食谱中,我们将学习如何将 C++ 时间点转换为 POSIX 时间戳,并从 POSIX 时间戳创建 C++ 时间点。

怎么做……

我们将创建一个应用,该应用将时间点转换为 POSIX 时间戳,然后从该时间戳恢复时间点。

  1. 在您的~/test工作目录中,创建一个名为timestamps的子目录。
  2. 使用您喜欢的文本编辑器在timestamps子目录中创建timestamps.cpp文件。
  3. 将以下代码片段放入文件中:
#include <iostream>
#include <chrono>

int main() {
  auto now = std::chrono::system_clock::now();

  std::time_t ts = std::chrono::system_clock::to_time_t(now);
  std::cout << "POSIX timestamp: " << ts << std::endl;

  auto restored = std::chrono::system_clock::from_time_t(ts);

  std::chrono::duration<double, std::milli> delta = now - restored;
  std::cout << "Recovered time delta " << delta.count() << std::endl;
  return 0;
}
  1. 创建包含我们程序的构建规则的CMakeLists.txt文件:
cmake_minimum_required(VERSION 3.5.1)
project(timestamps)
add_executable(timestamps timestamps.cpp)

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

SET(CMAKE_CXX_FLAGS "--std=c++ 11")
set(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)

现在您可以构建和运行应用了。

它是如何运作的..。

首先,我们使用系统时钟为当前时间创建一个 Time Point 对象:

auto now = std::chrono::system_clock::now();

由于 POSIX 时间戳表示自纪元以来的时间,因此我们不能使用稳定时钟。 但是,系统时钟知道如何将其内部表示形式转换为 POSIX 格式。 为此,它提供了一个to_time_t静态函数:

std::time_t ts = std::chrono::system_clock::to_time_t(now);

结果被定义为具有类型std::time_t,但这是一个整数类型,而不是对象。 与时间点实例不同,我们可以将其直接写入输出流:

std::cout << "POSIX timestamp: " << ts << std::endl;

让我们尝试从这个整数时间戳恢复一个时间点。 我们使用from_time_t静态函数:

auto restored = std::chrono::system_clock::from_time_t(ts);

现在,我们有两个时间戳。 它们是一样的吗? 让我们计算并显示差值:

std::chrono::duration<double, std::milli> delta = now - restored;
std::cout << "Recovered time delta " << delta.count() << std::endl;

当我们运行应用时,我们会得到以下输出:

时间戳是不同的,但差异始终小于 1,000。 由于 POSIX 时间戳被定义为自纪元以来的秒数,因此我们丢失了精细粒度时间,如毫秒和微秒。

尽管有这些限制,POSIX 时间戳仍然是一种重要且广泛使用的时间传输表示形式,我们学习了在需要时如何将它们转换为内部 C++ 表示形式。

还有更多的..。

在许多情况下,直接使用 POSIX 时间戳就足够了。 因为它们是用数字表示的,所以可以使用简单的数字比较来确定哪个时间戳是新的还是旧的。 同样,从一个时间戳中减去另一个时间戳得到它们之间的时间间隔(以秒为单位)。 如果性能是瓶颈,则此方法可能比与本机 C++ 时间点进行比较更可取。