1

I'm using Boost Asio for reading my USB data. It works great! But I have some conserns.

I'm using synchronous reading (blocking read) when I want to read an incoming message. The reason why I'm choosing blocking read is because I want to read the message directly when it arrives. I have a feeling that if I'm using asynchronous (Non blocking read) read, then I might loose the first data of the incoming data.

My code for reading data is

// Atomic variable for the thread
std::atomic<bool> dataReceived(false);

// Start a read thread
std::thread readThread([&]() {
    constexpr std::size_t buffer_size = 1024;
    std::array<char, buffer_size> buffer;

    boost::system::error_code error;
    std::size_t bytes_transferred = 0;
    try {
        bytes_transferred = deviceUSB.read_some(boost::asio::buffer(buffer), error);
    }
    catch (...) {}

    if (!error) {
        dataRX.assign(buffer.begin(), buffer.begin() + bytes_transferred);
        dataReceived = true;
    }
});

// Write data
deviceUSB.write_some(boost::asio::buffer(dataTX, size));

// Check if data has been received or timeout has occurred
bool timeout = false;
auto startTime = std::chrono::steady_clock::now();
auto timeoutDuration = std::chrono::milliseconds(timeOutMilliseconds);
while (!dataReceived && !timeout) {
    auto currentTime = std::chrono::steady_clock::now();
    auto elapsedTime = std::chrono::duration_cast<std::chrono::milliseconds>(currentTime - startTime);

    // Check if timeout has occurred
    if (elapsedTime >= timeoutDuration) {
        timeout = true;
        readThread.detach(); // Kill the thread
    }
}

// End the thread
if (readThread.joinable()) {
    readThread.join();
}

And the problem is this line. I don't actually kill the thread, just detaching it.

 readThread.detach(); // Kill the thread

If I detach it, then the thread will still live on. My suggestion is to use a while-loop instead of a thread, together with asynchronous read. If the message does not arrive within a certain limit, then the while loop will end.

Question:

Is asynchronous + while loop

bool run = true;
while(run){
    bytes_transferred = deviceUSB.async_read_some(boost::asio::buffer(buffer), error);
    if(bytes_transferred > 0){
        run = false;
    }
}

the same as synchronous inside Boost Asio?

bytes_transferred = deviceUSB.read_some(boost::asio::buffer(buffer), error);
sehe
  • 374,641
  • 47
  • 450
  • 633
euraad
  • 2,467
  • 5
  • 30
  • 51

1 Answers1

1

Is asynchronous + while loop [...] the same as synchronous inside Boost Asio?

The question does not make sense. A while loop doesn't make sense in the context of async_read_some. For starters, async_read_some does not return bytes_read so you cannot use it as the loop condition.

You can of course chain multiple async operations, this is often known as a composed operation. That way your while loop can be mimicked in asynchronous code.

To get fully meta, using coroutines you can again write that as a while loop, even though the execution gets suspended and resumed just like with completion handler chaining.

To your concerns:

Buffering

yes repeatedly posting new async read operations will get you all the data, just like when using blocking reads. That's because serial_port models AsyncReadStream which is documented to be "Buffer-oriented asynchronous read stream". In other words, there is internal buffering. In practice the buffering may take place in kernel/hardware, but it's there nonetheless.

Timeout

This also makes little sense:

readThread.detach(); // Kill the thread

detach() doesn't do that. It just tells the OS you're no longer interested in the thread, but you should be as you are sharing resources with the thread.

You don't voice this as a concern, but you should. There is no useful timeout on a blocking read! The call may simply block indefinitely, and your timeout detection might be useless as a result.

Instead, consider asynchronous read, combined with an asynchronous timer wait. That way you can cancel the read when the timer completes, or cancel the timer when the read completes.

Also consider using a composed read. E.g. your wording "I might loose the first data of the incoming data" gives me the idea that you might want to await multiple "packets" until a message is complete. Instead, you can use asio::[async_]read_until to simply read until the condition is met.

E.g. you can read_until(deviceUSB, buf_, "\r\n") to have ASIO do multiple read_some` operations until the buffer contains the delimiter sequence ("\r\n") or an error occurred.

More complicated forms of framing can be used, like e.g. regular expressions, length-prefixed, control-codes etc. I think I have some examples on this site, looking... This seems close Boost ASIO System timer spurious timeout (at least shows in detail how to use an async timer for mutual cancellation, also async_read_until for more complicated completion condition). Here's an example that reads until ETX: asio_read_some countinue reading to get all data I can't find a more complex example that I seem to remember. I'll add it if I find it later.

Sample

Here's a simple example of what I think a good read call would look like for you:

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

int main() {
    asio::io_context ioc;
    asio::serial_port deviceUSB(ioc, "something");
    asio::steady_timer timer(ioc, 3s);

    std::vector<char> buf;

    timer.async_wait([&](error_code ec) {
        std::cerr << "Timer completion (" << ec.message() << ")" << std::endl;
        if (!ec) {
            std::cerr << "Timeout expired" << std::endl;
            deviceUSB.cancel(); // cancels any async operation
        }
    });

    async_read_until(deviceUSB, asio::dynamic_buffer(buf), "\r\n",
                     [&](error_code ec, size_t bytes_transferred) {
                         std::cerr << "Read completion (" << ec.message() << ", " << bytes_transferred
                                   << " bytes)" << std::endl;
                         if (!ec.failed())
                             timer.cancel();
                     });

    ioc.run();
}

If you need multiple read operations, you might consider keeping the io context around (e.g. using asio::thread_pool(1)).

sehe
  • 374,641
  • 47
  • 450
  • 633
  • Wow! Great post! So by using a for-loop together with `asyn_read_some`, it will guarantee me that I can read the whole incoming message? Sorry, I don't have `async_read_until`. – euraad Jul 16 '23 at 21:08
  • How long does the data stay in the buffer for the async reading? It depends on the operative system? – euraad Jul 16 '23 at 21:29
  • As long as you have the object opened you can usually trust the OS to keep the state - that's what the object handles are for. There might be obscure exceptions/flags that create exceptions, but I've never encountered them before. – sehe Jul 16 '23 at 22:04
  • And of course you have [`async_read_until`](https://www.boost.org/doc/libs/1_82_0/doc/html/boost_asio/reference/async_read_until.html). It's been part of Asio since forever. My answer code compiles as is. – sehe Jul 16 '23 at 22:06
  • What do you mean by "object opened" ? – euraad Jul 17 '23 at 13:24
  • When you open an IO object, for example a stream socket, datagram socket, file descriptor, serial port etc. It's a contract with the kernel. You tell it "I want to use it" and then when that succeeded "I'm using this". Until you close it, after which the contract ends, obviously – sehe Jul 17 '23 at 13:34
  • Sorry. I don't have the function `async_read_until` from So this will open the object? https://pastebin.com/iSHdEMJG – euraad Jul 17 '23 at 15:43
  • Again, of course you must have `async_read_until`. I linked you to the documentation, you have it. And no, that won't open the object. What opens it is opening. Like usually with [`open()`](https://beta.boost.org/doc/libs/1_82_0/doc/html/boost_asio/reference/basic_serial_port/open.html) or in some cases during construction (the [documentation will state when it does](https://beta.boost.org/doc/libs/1_82_0/doc/html/boost_asio/reference/basic_serial_port/basic_serial_port.html)) – sehe Jul 17 '23 at 18:13
  • The beauty of a well-designed API is that things have descriptive names :) – sehe Jul 17 '23 at 18:14
  • Thank you for your answer. unfortunately I don't really understand the documentation. When I try to access `async_read_until`, it does not appear. I have included `#include ` as well. Does not exist for me. :( Do you want to try that code? Here it is. https://pastebin.com/AxFNR85A – euraad Jul 17 '23 at 19:26
  • Things don't "appear". Don't lean on an IDE to accidentally discover API's. You can see my example code, which, again, compiles. You can see for yourself: http://coliru.stacked-crooked.com/a/7a4611e4e4b743d4 or https://godbolt.org/z/hfvqGvs91. As a hint: is `async_read_until` a member function? If so, of what type? If not, in what namespace is it? – sehe Jul 17 '23 at 19:34
  • It should appear. Or else Visual Studio might give me an error. No, `async_read_until` is not a member function for me. If you see my code, you can see that I'm using the name space `boost::asio::serial_port`. I like Boost Asio, but it seems so diffucult to handle the USB port. – euraad Jul 17 '23 at 20:39
  • @euraad I was **asking**. Is it a member function? What does the documentation say about that? Regarding the code ("Do you want to try that code")... what is there to try? It's not self-contained. I simplified and made it self-contained. I fixed **numerous** issues along the way, give it a thorough read: http://coliru.stacked-crooked.com/a/f360b4fcc817ef6c – sehe Jul 17 '23 at 20:57
  • Wow! You are a truly C++ programmer :) I will read your code – euraad Jul 17 '23 at 21:46
  • I think you have misstaken `auto bytes_transferred = usbPort->read_some(asio::buffer(dataRX), error);`. This will block the reading. – euraad Jul 17 '23 at 22:51
  • Oh. Yeah, that was the most important fix. What you had was infinite `async_read_some` in a while loop. Clear-cut [undefined behavior](https://en.wikipedia.org/wiki/Undefined_behavior) because you cannot have overlapping reads. Just, don't do while loops with async. My answer started out with **exactly** that explanation. – sehe Jul 17 '23 at 22:56
  • Ok. While loops with `async_read_some` is a bad idea. Got it. So I will try to use the `ioc.run();` method? – euraad Jul 17 '23 at 23:06
  • But still. I don't have `async_read_until` :) – euraad Jul 17 '23 at 23:06
  • You don't use it. The code doesn't have any logic that makes sense to express with it. Then again, there is no logic at all: your pastebin had only undefined behaviour, and you're still only reading "some" - where "some" is whatever the OS/driver will deliver in one read, with a max of 1024. Oh, and when you encounter an error, you will just keep reading, overwriting the buffer until timeout. Oh, and that's assuming the OS/driver will actually return from the read at all. – sehe Jul 17 '23 at 23:18
  • You can replace the code with something that does make more sense, like e.g. shown in my answer. And, unsurprisingly then the async_read_until *does* come in handy. And I won't reiterate that you *have it* because that was [established 3h ago](https://stackoverflow.com/questions/76698830/is-synchronous-read-the-same-as-asynchronous-read-while-loop-inside-boost-asio/76699224?noredirect=1#comment135237024_76699224). You're just not looking in the right place. – sehe Jul 17 '23 at 23:20
  • Note, the line commented with `// SEHE: not write_some!`. Note how it **correctly** uses the free function `asio::write` instead of `serial_port::write_some`. If you read the documentation for `write_some` you'll realize you'd have to check how much was actually sent, and resend the remaining part of the buffer if applicable. This is the **exact** same relation that `asio::read_XXX` has to `serial_port::read_some`. Note how you *do* have `asio::write`. In exactly the same way, same place, and same use as `asio::read_XXX`. (Same for `async_[read|write]_XXXX`) – sehe Jul 17 '23 at 23:22
  • I need to try out your example. I got my IDE to recognize the `async_read_some` now. It was the name spaces that causing some issues. I don't like name space. Here is what I would write. It's your code by the way, but I added `boost::asio::write` before `io.run()` thread. https://pastebin.com/9psfwj8e – euraad Jul 18 '23 at 06:40
  • By the way! Do I really need the delimeter `"\r\n"` ? Because I'm sending raw data. – euraad Jul 18 '23 at 06:54
  • Ok, now I understand what I need the delimeter for :) – euraad Jul 18 '23 at 07:23
  • I don't :) If you're sending "raw data" that's pretty much the antithesis of line-delimited. Like I said in my answer _"you might want to await multiple "packets" until a message is complete. Instead, you can use asio::[async_]read_until to simply read until the condition is met"_. In your case you need to define when a message is complete. In the quote-block I have several other examples for inspiration. – sehe Jul 18 '23 at 10:30
  • Looking at the new pastebin, that couldn't possibly compile, unless you changed the type of `devicesCDC`, in which case you might have created a new set of problems. Also, you don't restart the service [as per documentation](https://www.boost.org/doc/libs/develop/doc/html/boost_asio/reference/io_context/restart.html). Here's how I'd integrate that into the reviewed version: https://paste.ubuntu.com/p/JnvYvfhWsF/ – sehe Jul 18 '23 at 14:08
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/254540/discussion-between-sehe-and-euraad). – sehe Jul 18 '23 at 14:08