简体   繁体   中英

How to correctly interrupt boost::asio::read on a boost::asio::serial_port under Linux?

The code is very straitforward, just opening a socket, starting a synchronous read thread and trying to stop it.

#include <iostream>

#include <boost/asio.hpp>

#include <thread>
#include <chrono>

void readThread(boost::asio::serial_port* port)
{
    static const size_t maxBytesToRead = 256;
    unsigned char buffer[maxBytesToRead];
    size_t read = 0;
    while (true)
    {
        try
        {
            std::cout << "readThread reading" << std::endl;
            read = boost::asio::read(*port,
                        boost::asio::buffer(buffer, maxBytesToRead));
        }
        catch (...)
        {
            std::cout << "exception in readThread, about to stop" << std::endl;
            return;
        }
    }
}

int main( int argc, char* argv[] )
{
    boost::asio::io_service service;
    boost::asio::serial_port port(service);

    //port.open("/dev/ttyACM0");
    port.open("COM9");
    port.set_option(boost::asio::serial_port_base::baud_rate(115200));
    port.set_option(boost::asio::serial_port_base::parity(boost::asio::serial_port_base::parity::none));
    port.set_option(boost::asio::serial_port_base::stop_bits(boost::asio::serial_port_base::stop_bits::one));
    port.set_option(boost::asio::serial_port_base::character_size(8));
    port.set_option(
            boost::asio::serial_port_base::flow_control(
                boost::asio::serial_port_base::flow_control::hardware));

    std::thread thrd(&readThread, &port);

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

    std::cout << "Calling cancel()" << std::endl; 
    port.cancel();
    std::cout << "Called cancel()" << std::endl;

    thrd.join();

    std::cout << "Exiting, OK" << std::endl;

    return 0;
}

This works perfectly fine under Windows, and outputs:

readThread reading
Calling cancel()
Called cancel()
exception in readThread, about to stop
Exiting, OK

But, under Linux, the read operation in thread does not get interrupted, output is:

readThread reading
Calling cancel()
Called cancel()

and then the application hangs forever.

What's the right strategy to interrupt this synchronous read? I'd like not to move to async read as I'm porting the app to Linux and wish to do miniam changes in the code structure.

Cancellation requires async read. You used a non-portable undocumented effect for serial_port::cancel on windows.

What's worse, calling cancel on the port is a data race, so you have Undefined Behaviour as well.

Instead of a thread, make that an async read chain:

using read_state = std::array<unsigned char, 256>;

void readThread(asio::serial_port& port, std::shared_ptr<read_state> state = {}) {
    if (!state) {
        state = std::make_shared<read_state>(); // shared for the entire async chain
    }
    async_read(port, asio::buffer(*state),
               [&port, state](error_code ec, size_t bytes_read) {
                   std::cout << "received " << bytes_read << " (" << ec.message() << ")"
                             << std::endl;
                   if (!ec) {
                       readThread(port, state);
                   }
               });
}

Now you can rewrite main :

int main(int argc, char* argv[]) {
    asio::io_service service;
    using asio::serial_port;
    serial_port port(service, argc > 1 ? argv[1] : default_device);

    port.set_option(serial_port::baud_rate(115200));
    port.set_option(serial_port::parity(serial_port::parity::none));
    port.set_option(serial_port::stop_bits(serial_port::stop_bits::one));
    port.set_option(serial_port::character_size(8));
    port.set_option(serial_port::flow_control(serial_port::flow_control::hardware));

    readThread(port);

    // preferrably with a timer, or on Ctrl-C
    std::thread([&port] {
        std::this_thread::sleep_for(std::chrono::seconds(3));

        std::cout << "Calling cancel()" << std::endl;
        post(port.get_executor(), [&] { port.cancel(); });
        std::cout << "Called cancel()" << std::endl;
    }).detach();

    service.run();

    std::cout << "Exited, OK" << std::endl;
}

Or, by using a thread pool, avoid the manual thread:

int main(int argc, char* argv[]) {
    asio::thread_pool service(1);
    using asio::serial_port;
    serial_port port(make_strand(service), argc > 1 ? argv[1] : default_device);

    port.set_option(serial_port::baud_rate(115200));
    port.set_option(serial_port::parity(serial_port::parity::none));
    port.set_option(serial_port::stop_bits(serial_port::stop_bits::one));
    port.set_option(serial_port::character_size(8));
    port.set_option(serial_port::flow_control(serial_port::flow_control::hardware));

    readThread(port);

    // preferrably with a timer, or on Ctrl-C
    std::this_thread::sleep_for(std::chrono::seconds(3));

    std::cout << "Calling cancel()" << std::endl;
    post(port.get_executor(), [&] { port.cancel(); });
    std::cout << "Called cancel()" << std::endl;

    service.join();

    std::cout << "Exited, OK" << std::endl;
}

Several Improvements + Demo

Suggesting to eg exit on a timer or signal instead of from a background thread, adding some tracing and a live demo:

Live On Coliru

#include <boost/asio.hpp>
#include <chrono>
#include <iomanip>
#include <iostream>
#include <thread>
namespace asio = boost::asio;
using boost::system::error_code;
using namespace std::chrono_literals;

static auto const  start = std::chrono::steady_clock::now();
static inline void trace(auto const&... args) {
    std::cout << std::setw(8) << (std::chrono::steady_clock::now()-start)/1ms << "ms ";
    (std::cout << ... << args) << std::endl;
}

#ifdef _WIN32
    auto constexpr static default_device = "COM9";
#else
    auto constexpr static default_device = "/dev/ttyACM0";
#endif

struct read_state {
    std::array<unsigned char, 256> buffer;
    size_t                         total_bytes = 0;
};

void readThread(asio::serial_port& port, std::shared_ptr<read_state> s = {}) {
    if (!s) {
        s = std::make_shared<read_state>(); // shared for the entire async chain
    }
    //port.async_read_some(
    async_read(port, //
               asio::buffer(s->buffer), [&port, s](error_code ec, size_t bytes_read) {
                   s->total_bytes += bytes_read;
                   trace("received ", bytes_read, "/", s->total_bytes / 1024.0 / 1024,
                         "Mb (", ec.message(), ")");

                   if (!ec) {
                       readThread(port, s); // continue reading
                   }
               });
}

int main(int argc, char* argv[]) {
    std::cout << std::setprecision(2) << std::fixed;

    asio::thread_pool service;
    auto strand = make_strand(service);

    using asio::serial_port;
    serial_port port(strand, argc > 1 ? argv[1] : default_device);

    asio::signal_set sig(service, SIGINT, SIGTERM);
    sig.async_wait([&](error_code ec, int num) {
        trace("signal ", num, "(", ec.message(), ")");
        if (!ec) {
            post(strand, [&] { port.close(); });
            trace("cancelled");
        }
    });

    port.set_option(serial_port::baud_rate(115200));
    port.set_option(serial_port::parity(serial_port::parity::none));
    port.set_option(serial_port::stop_bits(serial_port::stop_bits::one));
    port.set_option(serial_port::character_size(8));
    port.set_option(serial_port::flow_control(serial_port::flow_control::hardware));

    readThread(port);

    service.join();

    trace("Exited, OK");
}

在此处输入图像描述

CAVEAT

Note the subtlety that I used port.close() in that last example instead of port.cancel() . Using cancel() risked a race condition where the read thread will not exit when there the cancel() arrives at the exact time that there is no async read in flight (instead the completion handler is pending).

If you prefer cancel() for purity, you will need additional state, like a cancelled flag, like so: Live On Coliru . Notice how sometimes the last read ends with (Success) and sometimes with (Operation canceled) : https://imgur.com/a/vjTv5CY

Another option might be to use asio::cancellation_slot

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM