我们有一个DLL,它为我们制作的USB设备提供API,可以显示为USB CDC com端口。我们实际上在Windows上使用自定义驱动程序以获得最佳性能以及异步i / o,但我们过去也使用了串口异步文件i / o,并且也取得了相当的成功。
在与我们的设备通信时,延迟在此API中非常重要,因此我们构建了库,以便当应用程序进行API调用以在设备上执行命令时,这些命令将直接转换为API调用者线程上的写入没有等待上下文切换。该库还维护一个监听线程,该线程始终在异步读取时使用等待对象等待新响应。这些响应被解析并插入到线程安全的队列中,供API用户在方便时阅读。
所以基本上,我们在API调用者的线程中完成了大部分的写作,并且我们在一个监听线程中读取了所有内容。我已经尝试将我们的代码版本移植到使用QSerialPort而不是Windows和OSX的本机串行文件i / o,但每当我尝试从调用者的线程写入()时,我都会遇到错误(QSerialPort是在听力线程):
QObject: Cannot create children for a parent that is in a different thread.
这似乎是由于为QSerialPortPrivate :: startAsyncWrite()使用的通知程序池创建了另一个基于QObject的WriteOverlappedCompletionNotifier。
QSerialPort的当前5.2版本是否仅限于在同一线程上进行读写操作?这似乎非常不幸,因为底层操作系统对串行端口文件i / o没有任何此类线程限制。据我所知,问题主要与所有QSerialPort的通知程序类都基于QObject的事实有关。
有没有人对此有好的工作?我可能会尝试构建我自己的QSerialPort,它使用不基于QObject的通知程序来查看这有多远。 QObject似乎在这里给出的唯一真正优势是在端口关闭时破坏通知程序。
答案 0 :(得分:3)
您可以自由检查QSerialPort
和QIODevice
代码,看看需要更改哪些内容才能使write
方法可以从一个线程进行线程安全访问。通知程序根本不需要是QSerialPort
的子项,它们可以添加到在销毁时清理的指针列表中。
我的猜测是,主线代码可能不需要进行其他更改,只需要使用互斥保护来访问错误状态,但您需要确认。这对您的代码影响最小。
如果您关心发布完整性,那么您应该自己编译Qt,并且您应该将它作为您自己的源代码存储库的一部分。所以这一切都不应该是任何问题。
“这些命令直接转换为API调用者线程上的写入,因此无需等待上下文切换”现代机器是多核的,多个线程当然可以并行运行而无需任何上下文切换。但根本问题是:为什么要这么麻烦?如果您需要硬实时保证,则需要一个硬实时系统。否则,系统中的任何内容都不应该关心这种微不足道的延迟。如果您这样做只是为了让GUI感觉响应,那么这样的过度复杂就没有意义了。
我所做的,取得了巨大的成功和出色的性能,是将通信协议和通信端口放在同一个专用线程中,以及GUI线程或其他线程中的用户。通信端口通常为QIODevice
,如QTcpSocket
,QSerialPort
,QLocalSocket
等。由于通信协议对象“只是”QObject
,因此也可以使用端口在GUI线程中实现演示目的 - 无论如何它都是完全异步设计的,除了最简单的计算外,它不会阻塞任何东西。
通信协议将多个执行请求排队。即使在单核机器上,一旦GUI线程完成提交所有请求,进一步的执行都在通信线程中。
QSerialPort
实现使用异步OS API。在单独的线程上进一步处理这些异步回复几乎没有任何好处。这些操作的开销非常低,并且通过尝试这样做,您将无法获得可测量的延迟。请记住:这不是您的代码,而只是在缓冲区之间推送字节的代码。是的,在负载很重或单核系统上可能存在上下文切换开销,但除非你可以测量它的存在和不存在之间的差异,否则你就会遇到想象中的问题。
当然,只要您通过事件队列互斥锁序列化对它的访问,就可以从多个线程中使用任何 QObject
。无论何时使用QMetaObject::invokeMethod
或信号槽连接,都可以使用此功能。
因此,在QSerialPort
周围添加一个简单的包装器,将write
公开为线程安全方法。在内部,它应该使用信号槽连接。您可以从任何线程调用此线程安全的write
。这种调用的开销是互斥锁和2+n
malloc / free调用,其中n
是非零数量的参数。
在您的包装器中,您还可以处理readyRead
信号,并发出包含接收数据的信号。该信号可以由居住在另一个线程中的QObject
处理。
总的来说,如果你正确地进行了测量,并且你的端口线程的实现是正确的,那么对于所有这些并发症你都应该没有任何好处。
如果您的通信协议进行大量数据处理,则应将其考虑在内。它可以进入一个单独的QObject
,然后可以在自己的线程上运行。或者,可以使用由QtConcurrent::run
执行的专用仿函数来完成。
答案 1 :(得分:0)
如果使用QSerialPort
打开并配置串行端口,QSocketNotifier
监视读取活动(以及其他QSocketNotifier
实例的写入完成和错误处理,如果需要),该怎么办? ?
QSerialPort::handle
应该为您提供所需的文件描述符。在Windows上,如果该函数返回Windows HANDLE
,则可以使用_open_osfhandle来获取文件描述符。
答案 2 :(得分:0)
作为后续工作,在讨论后不久,我使用select()等为POSIX系统实现了自己的线程安全串口代码,并且它在Qt和非Qt应用程序的多个线程上运行良好一样。基本上,我完全放弃了使用QtSerialPort。