我使用 boost::asio 进行串行通信,我想监听某个端口上的传入数据。所以,我使用注册一个 ReadHandlerserialport::async_read_some()
然后创建一个单独的线程来处理异步处理程序(调用io_service::run()
)。我的 ReadHandler 通过再次调用在其末尾重新注册自身async_read_some()
,这似乎是一种常见的模式。
这一切都有效,我的示例可以在接收到数据时将其打印到标准输出 - 但我注意到在 ReadHandler 运行时接收到的数据将不会被“读取”,直到 ReadHandler 执行完毕并且之后会收到新数据。也就是说,当ReadHandler运行时接收到数据时,虽然在ReadHandler结束时调用了async_read_some,但它不会立即再次调用该数据的ReadHandler。 ReadHandler 仅在以下情况下才会被再次调用:额外的初始 ReadHandler 完成后接收数据。此时,ReadHandler 运行时接收到的数据将与“新”数据一起正确地存储在缓冲区中。
这是我的最小可行示例 - 我最初将其放入 Wandbox 中,但意识到它无助于在线编译它,因为无论如何它都需要串行端口才能运行。
// Include standard libraries
#include <iostream>
#include <string>
#include <memory>
#include <thread>
// Include ASIO networking library
#include <boost/asio.hpp>
class SerialPort
{
public:
explicit SerialPort(const std::string& portName) :
m_startTime(std::chrono::system_clock::now()),
m_readBuf(new char[bufSize]),
m_ios(),
m_ser(m_ios)
{
m_ser.open(portName);
m_ser.set_option(boost::asio::serial_port_base::baud_rate(115200));
auto readHandler = [&](const boost::system::error_code& ec, std::size_t bytesRead)->void
{
// Need to pass lambda as an input argument rather than capturing because we're using auto storage class
// so use trick mentioned here: http://pedromelendez.com/blog/2015/07/16/recursive-lambdas-in-c14/
// and here: https://stackoverflow.com/a/40873505
auto readHandlerImpl = [&](const boost::system::error_code& ec, std::size_t bytesRead, auto& lambda)->void
{
if (!ec)
{
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - m_startTime);
std::cout << elapsed.count() << "ms: " << std::string(m_readBuf.get(), m_readBuf.get() + bytesRead) << std::endl;
// Simulate some kind of intensive processing before re-registering our read handler
std::this_thread::sleep_for(std::chrono::seconds(5));
//m_ser.async_read_some(boost::asio::buffer(m_readBuf.get(), bufSize), lambda);
m_ser.async_read_some(boost::asio::buffer(m_readBuf.get(), bufSize), std::bind(lambda, std::placeholders::_1, std::placeholders::_2, lambda));
}
};
readHandlerImpl(ec, bytesRead, readHandlerImpl);
};
m_ser.async_read_some(boost::asio::buffer(m_readBuf.get(), bufSize), readHandler);
m_asioThread = std::make_unique<std::thread>([this]()
{
this->m_ios.run();
});
}
~SerialPort()
{
m_ser.cancel();
m_asioThread->join();
}
private:
const std::chrono::system_clock::time_point m_startTime;
static const std::size_t bufSize = 512u;
std::unique_ptr<char[]> m_readBuf;
boost::asio::io_service m_ios;
boost::asio::serial_port m_ser;
std::unique_ptr<std::thread> m_asioThread;
};
int main()
{
std::cout << "Type q and press enter to quit" << std::endl;
SerialPort port("COM1");
while (std::cin.get() != 'q')
{
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
return 0;
}
(不要介意正在发生的奇怪的 lambda 事情)
该程序只是将收到的数据以及时间戳(自程序启动以来的毫秒数)打印到标准输出。通过将虚拟串行设备连接到虚拟串行端口对,我可以将数据发送到程序(实际上只需输入 RealTerm)。当我输入短字符串时我可以看到问题。
在本例中,我输入“hi”,然后立即打印“h”。我很快就输入了“i”,但以计算机速度来说,这需要相当长的时间,因此它不是读入缓冲区的初始数据的一部分。此时,ReadHandler 执行,需要 5 秒。在此期间,操作系统收到了“i”。但是 5 秒后并没有打印“i”——下一个 async_read_some 会忽略它,直到我输入“t”,此时它突然打印“i”和“t”。示例程序输出 https://i.stack.imgur.com/RC8NN.png
这是此测试以及我想要的内容的更清晰描述:
测试:启动程序,等待 1 秒,输入 hi,等待 9 秒,输入 t
我想要发生什么(由该程序打印到标准输出):
1000毫秒:小时
6010ms:我
11020ms:t
实际发生的情况:
1000毫秒:小时
10000ms:它
程序有一种方法来识别在读取之间接收到的数据似乎非常重要。我知道无法使用 ASIO 串行端口(无论如何不使用 native_handle)检查数据是否可用(在操作系统缓冲区中)。但我真的不需要,只要读取调用返回即可。此问题的一种解决方案可能是确保 ReadHandler 尽快完成运行 - 显然此示例中的 5 秒延迟是人为的。但这对我来说并不是一个好的解决方案;无论我使 ReadHandler 的速度有多快,仍然有可能“错过”数据(因为直到稍后收到一些新数据后才会看到它)。有什么办法可以ensure我的处理程序将在收到数据后的短时间内读取所有数据,而不依赖于进一步数据的接收?
我已经在 SO 和其他地方进行了大量搜索,但到目前为止我发现的所有内容都只是讨论导致系统根本无法工作的其他陷阱。
作为一种极端措施,看起来我的工作线程可能会调用io_service::run_for()
有超时,而不是run()
,然后每隔一小会儿就会让该线程以某种方式触发手动读取。我还不确定会采取什么形式 - 它可以直接调用serial_port::cancel()
我想,然后重新调用async_read_some
。但这对我来说听起来很老套,即使它可能有效 - 并且它需要更新版本的 boost 才能启动。
我正在使用 VS2019 在 Windows 10 上使用 boost 1.65.1 进行构建,但我真的希望这与这个问题无关。