如何在不等待新数据到达的情况下使用 boost::asio 的 async_read_some() 读取所有可用数据?

How can I read all available data with boost::asio's async_read_some() without waiting for new data to arrive?

我正在使用 boost::asio 进行串行通信,我想监听某个端口上的传入数据。因此,我使用 serialport::async_read_some() 注册了一个 ReadHandler,然后创建了一个单独的线程来处理异步处理程序(调用 io_service::run())。我的 ReadHandler 通过再次调用 async_read_some() 在其末尾重新注册自己,这似乎是一种常见的模式。

这一切都有效,我的示例可以在收到数据时将数据打印到标准输出 - 除了我注意到在 ReadHandler 运行ning 时收到的数据不会 'read' 直到ReadHandler 完成执行并在此之后接收到新数据。也就是说,当ReadHandler在运行ning期间接收到数据时,虽然在ReadHandler结束时调用了async_read_some,但不会立即为该数据再次调用ReadHandler。只有在初始 ReadHandler 完成后收到 additional 数据时,才会再次调用 ReadHandler。此时,在 ReadHandler 运行ning 期间接收到的数据将与 'new' 数据一起正确地存储在缓冲区中。

这是我的最小可行示例 - 我最初将它放在 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: 
            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' 被 OS 接收。但是 'i' 不会在 5 秒后打印 - 下一个 async_read_some 会忽略它,直到我输入 't',此时它突然打印出 'i' ] 和 't'。 Example program output

下面是对这个测试和我想要的更清楚的描述:

测试:启动程序,等待1秒,输入hi,等待9秒,输入t
我想要发生的事情(通过这个程序打印到标准输出):
1000 毫秒:h
6010 毫秒:我
11020 毫秒:t

实际发生了什么:
1000 毫秒:h
10000 毫秒:它

程序能够识别读取之间接收到的数据似乎非常重要。我知道无法使用 ASIO 串行端口(无论如何不使用 native_handle)检查数据是否可用(在 OS 缓冲区中)。但我真的不需要,只要读取调用returns。此问题的一种解决方案可能是确保 ReadHandler 尽快完成 运行ning - 显然,此示例中的 5 秒延迟是人为设计的。但这并不是一个好的解决方案。无论我使 ReadHandler 多快,它仍然可以 'miss' 数据(因为它不会被看到,直到稍后收到一些新数据)。有什么方法可以确保 我的处理程序将在收到数据后的短时间内读取所有数据,而不依赖于进一步数据的接收?

我在 SO 和其他地方做了很多搜索,但到目前为止我所发现的一切都只是在讨论导致系统根本无法工作的其他陷阱。

作为一种极端的措施,看起来我的工作线程可以超时调用 io_service::run_for(),而不是 run(),然后每隔一段时间就会以某种方式触发该线程手册阅读。我不确定会采用什么形式 - 我想它可以只调用 serial_port::cancel(),然后重新调用 async_read_some。但这对我来说听起来很老套,即使它可能有效——而且它需要更新版本的 boost 才能启动。

我正在使用 VS2019 在 Windows 10 上使用 boost 1.65.1 进行构建,但我真的希望这与这个问题无关。

回答标题中的问题:你不能。根据 async_read_some 的性质,您要求部分读取并在读取任何内容后立即调用您的处理程序。然后,在调用另一个 async_read_some 之前,您会睡很长时间。

no matter how fast I make ReadHandler, it will still be possible to 'miss' data (in that it will not be seen until some new data is received later)

如果我没有正确理解你的顾虑,没关系 - 你不会错过任何东西。数据仍在等待 socket/port 缓冲区,直到您下次读取它。

如果您只想在读取完成后开始处理,则需要 async_read 重载之一。这实际上将在流上执行多个 read_somes 直到满足某些条件。这可能只意味着 port/socket 上的 一切 ,或者您可以提供一些自定义 CompletionCondition。这在每个 read_some 上调用,直到它 returns 0,此时读取被认为完成,然后调用 ReadHandler