【问题标题】:QSerialPort::waitForBytesWritten "subtle errors" with an event loop?QSerialPort::waitForBytesWritten 带有事件循环的“细微错误”?
【发布时间】:2016-05-27 17:25:22
【问题描述】:

an answer on another question 中,声称当使用带有QSerialPort 的主Qt 事件循环时,QSerialPort::waitForBytesWritten 会导致“细微错误”,因为它会打开另一个事件循环。

我正在使用waitForBytesWritten 方法来确保在写入所有字节之前不会重新进入我的主事件循环。这不是一种有效的方法吗?会出现哪些“细微错误”?我应该改用flush(),这会确保在写入所有待处理字节之前不会再次输入我的事件循环吗?

am在我的应用程序中看到一些与其串行端口通信相关的奇怪行为,但我不确定是哪个“层”通信导致了问题,所以它甚至可能不是与我对QSerialPort 的使用有关。目前,我对它的起源的唯一提示是 QSerialPort 在我看到不良行为开始前不久发出 ResourceError 。这个ResourceError 会不会和我使用waitForBytesWritten 有关?

我在 Debian 7 系统上使用 Qt 5.5.1。

【问题讨论】:

  • 在没有看到一些代码的情况下很难说出为什么ResourceError 会发生。
  • @KubaOber 我不确定在这种情况下我可以提供多少代码,因为它是一个相当复杂的应用程序,并且它是专有产品的一部分。即使我将其缩减为一些较小的示例,我也不确定哪些部分是相关的,并且我无法创建最小的可重现案例,因为通信层、串行通信通道另一侧的进程等是都是专有的。
  • .......但即使是了解什么,确切地说,甚至可以在硬件串行端口上触发 ResourceError 也会有所帮助。
  • 顺便说一句:请解释为什么你认为你需要waitForBytesWritten:你认为该操作在调用它之前和之后的状态方面的含义是什么。你想通过调用它来实现什么?
  • @KubaOber 主要是为了尽快发送有效数据包的确认,一旦我读过它们。对于此协议,确认只有 4 或 5 个字节,因此我们的想法是立即将它们写入底层设备驱动程序。

标签: c++ qt serial-port debian


【解决方案1】:

在 Unix 上,读取通知将 EAGAINEIOEBADF 映射到 ResourceError 并停止处理进一步的读取通知,直到您调用 waitFor 函数或直到您重新打开端口。这使得QSerialPort 在您重新打开或waitFor 之前无法读取。也许将EIOEBADF 映射到相同的错误是有意义的。但是,由于EAGAIN 是良性的,这样处理它看起来像是一个 Qt 错误。

要查看是否是问题所在,您可以清除错误并重新调用 waitForReadyRead(0)waitForBytesWritten(0) 并查看错误是否再次出现:

bool retryWaitForBytesWritten(int n, int retries = 10) {
  if (!dev.bytesToWrite()) return false;
  while (retries-- && !dev.waitForBytesWritten(n)) {
    if (dev.error() != QSerialPort::ResourceError)
      return false;
    dev.clearError(); // retry if it was a resource error
  }
  return true;
}

您主要不应该使用waitForBytesWritten,因为:

  1. 它并没有像你想象的那样做。它所确保的只是QSerialPort 中的内部写入缓冲区已通过使用write 系统调用将数据提供给操作系统而被清空。这并不意味着数据已经物理发送。

  2. 它可能会阻塞。不保证会屏蔽。

  3. 通常不需要通知写入何时完成。如果您有一个半双工(本质上)命令-响应协议,您不在乎何时完成发送,而是何时响应已到达或超时。

    如果您有一个发送轮询命令的流协议,并且不希望生成它们的速度超过设备可以使用它们的速度,那么您应该发出命令(并在数据结构中将它们标记为“已发出”),直到bytesToWrite 已超过高水位线。然后暂停添加新命令,直到收到bytesWritten 信号通知发送缓冲区处于低水位。 readyRead 槽匹配对发出命令的响应并适当地指示它们。

因此,理想情况下,您的代码应该包含零个 waitXxx 调用。我们周围的世界是异步的。你不等待事情,你对它们做出反应。在发生某些事情之前不要停止事件循环 - 那是倒退。在你等待的那件事之前可能会发生其他事情,直到那时你才忽略它们。即使您将 I/O 推送到单独的线程,您仍然会浪费整个线程只是阻塞。在顺序伪同步代码中处理重定向控制流的事件也很困难 - 一切最终都充满了(必要的!)错误检查。有时人们会为此使用异常,这可能没问题,但在大型代码库中很难正确执行,并且任何时候您希望将控制重定向到不在调用堆栈上的代码。当您将代码明确表示为分层状态机时,在面对异步事件(例如传入数据或错误)时,更容易推断代码的正确性。

设计您的系统,使其根据异步发生的事件进行处理。如果您的 UI 的某些方面必须指示正在发送的数据的状态,那就这样吧:不要等待,不要阻塞线程。

This answer 演示了如何使用状态机框架为基于QIODevice 的设备实现异步 I/O。设置好状态后,将它们与 UI 行为联系起来是一件相对容易的事情。请参阅以下答案:onetwothreefourfivesixseveneight

另外,我错了,我修正了另一个答案。 QSerialPort 不会重新进入事件循环,网络模块中的套接字也不会。

【讨论】:

  • 我不明白你为什么说使用函数“因为它阻塞”是总是错误的。这就是我使用它的原因;我想要阻塞直到字节被写入。我一次只写大约 8 或 10 个字节(通常只有 4 个),我希望这些字节尽快按字面意思写入,如果有错误我想立即知道。相信我,我的应用程序中没有什么比在我进行 waitForBytesWritten 调用时写入这些字节更重要的了,包括计时器等。
  • @KyleStrand 没关系,这不是您问题的根源。我认为您遇到了真正的 Qt 错误。您可能会争辩说,以异步方式编写代码是一个风格问题,我的经验是,一旦您的通信不那么琐碎,它就会导致可怕的意大利面条。即使对于简单的协议,异步方式也可以带来更多的声明式风格。但这不是重点。
  • 谢谢,到目前为止这很有帮助——我认为你对EAGAIN 的看法是正确的;这只是意味着在异步读取操作完成时没有可读取的内容,对吗? (或者......类似的东西?)还有一个部分你没有回答——假设我确实想要阻止一个小的“写”操作,flush 比@987654355 更可取@?
  • @KyleStrand flush() 更可取。如果您不是流式传输数据,那么等待它的意义不大 - 因为它不能确保端口实际传输了任何数据。在旧的 Athlon XP Linux 机器上和最近的 OS X 上,都带有我刚刚尝试过的 PCI 硬件端口,flush() 返回 before 对端口进行任何外部可见的更改引脚状态。如果您正在流式传输数据,那么您可以通过对bytesWritten 信号做出反应来强制使用缓冲区水印。这些不是设备缓冲区。它们是 Qt 缓冲区!
  • @KyleStrand 请注意,当您阻塞时,CPU 内核通常会重新分配以执行其他线程的工作(如果它们是可运行的)。在该核心上,您的线程数据从缓存等中流出。如果您对异步信号做出反应,您可以确保如果您的线程不处理端口,它可以为您的进程 而不是其他一些进程——当然,如果可以完成这样的工作。在您的情况下,这可能根本不重要,当然,这只是一个观察。在满载的核心上,阻塞确实会不可撤销地放弃挂壁时间。
猜你喜欢
  • 2023-03-03
  • 2014-09-12
  • 2023-03-21
  • 2016-01-01
  • 2017-07-07
  • 2020-11-01
  • 2012-05-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多