2

I have been working on implementing a half duplex serial driver by learning from a basic serial terminal example using boost::asio::basic_serial_port: http://lists.boost.org/boost-users/att-41140/minicom.cpp

I need to read asynchronously but still detect when the handler is finished in the main thread so I pass async_read_some a callback with several additional reference parameters in a lambda function using boost:bind. The handler never gets invoked but if I replace the async_read_some function with the read_some function it returns data without an issue.

I believe I'm satisfying all of the necessary requirements for this function to invoke the handler because they are the same for the asio::read some function which returns:

  1. The buffer stays in scope
  2. One or more bytes is received by the serial device
  3. The io service is running
  4. The port is open and running at the correct baud rate

Does anyone know if I'm missing another assumption unique to the asynchronous read or if I'm not setting up the io_service correctly?

Here is an example of how I'm using the code with async_read_some (http://www.boost.org/doc/libs/1_56_0/doc/html/boost_asio/reference/basic_serial_port/async_read_some.html):

void readCallback(const boost::system::error_code& error, size_t bytes_transfered, bool & finished_reading, boost::system::error_code& error_report, size_t & bytes_read)
{
    std::cout << "READ CALLBACK\n";
    std::cout.flush();
    error_report = error;
    bytes_read = bytes_transfered;
    finished_reading = true;
    return;
}

int main()
{
    int baud_rate = 115200;
    std::string port_name = "/dev/ttyUSB0";
    boost::asio::io_service io_service_;
    boost::asio::serial_port serial_port_(io_service_,port_name);
    serial_port_.set_option(boost::asio::serial_port_base::baud_rate(baud_rate));
    boost::thread service_thread_;
    service_thread = boost::thread(boost::bind(&boost::asio::io_service::run, &io_service_));

    std::cout << "Starting byte read\n";
    boost::system::error_code ec;

    bool finished_reading = false;
    size_t bytes_read;
    int max_response_size = 8;
    uint8_t read_buffer[max_response_size];
    serial_port_.async_read_some(boost::asio::buffer(read_buffer, max_response_size),
                     boost::bind(readCallback,
                                 boost::asio::placeholders::error,
                                 boost::asio::placeholders::bytes_transferred,
                                 finished_reading, ec, bytes_read));

    std::cout << "Waiting for read to finish\n";
    while (!finished_reading)
    {
        boost::this_thread::sleep(boost::posix_time::milliseconds(1));
    }

    std::cout << "Finished byte read: " << bytes_read << "\n";

    for (int i = 0; i < bytes_read; ++i)
    {
    printf("0x%x ",read_buffer[i]);
    }
}

The result is that the callback does not print out anything and the while !finished loop never finishes.

Here is how I use the blocking read_some function (boost.org/doc/libs/1_56_0/doc/html/boost_asio/reference/basic_serial_port/read_some.html):

int main()
{
    int baud_rate = 115200;
    std::string port_name = "/dev/ttyUSB0";
    boost::asio::io_service io_service_;
    boost::asio::serial_port serial_port_(io_service_,port_name);
    serial_port_.set_option(boost::asio::serial_port_base::baud_rate(baud_rate));
    boost::thread service_thread_;
    service_thread = boost::thread(boost::bind(&boost::asio::io_service::run, &io_service_));

    std::cout << "Starting byte read\n";
    boost::system::error_code ec;
    int max_response_size = 8;
    uint8_t read_buffer[max_response_size];
    int bytes_read = serial_port_.read_some(boost::asio::buffer(read_buffer, max_response_size),ec);

    std::cout << "Finished byte read: " << bytes_read << "\n";

    for (int i = 0; i < bytes_read; ++i)
    {
    printf("0x%x ",read_buffer[i]);
    }
}

This version prints from 1 up to 8 characters that I send, blocking until at least one is sent.

dsolimano
  • 8,870
  • 3
  • 48
  • 63
st3am
  • 23
  • 1
  • 3

2 Answers2

2

The code does not guarantee that the io_service is running. io_service::run() will return when either:

  • All work has finished and there are no more handlers to be dispatched
  • The io_service has been stopped.

In this case, it is possible for the service_thread_ to be created and invoke io_service::run() before the serial_port::async_read_some() operation is initiated, adding work to the io_service. Thus, the service_thread_ could immediately return from io_service::run(). To resolve this, either:

  • Invoke io_service::run() after the asynchronous operation has been initiated.
  • Create a io_service::work object before starting the service_thread_. A work object prevents the io_service from running out of work.

This answer may provide some more insight into the behavior of io_service::run().


A few other things to note and to expand upon Igor's answer:

  • If a thread is not progressing in a meaningful way while waiting for an asynchronous operation to complete (i.e. spinning in a loop sleeping), then it may be worth examining if mixing synchronous behavior with asynchronous operations is the correct solution.
  • boost::bind() copies its arguments by value. To pass an argument by reference, wrap it with boost::ref() or boost::cref():

    boost::bind(..., boost::ref(finished_reading), boost::ref(ec),
                boost::ref(bytes_read));
    
  • Synchronization needs to be added to guarantee memory visibility of finished_reading in the main thread. For asynchronous operations, Boost.Asio will guarantee the appropriate memory barriers to ensure correct memory visibility (see this answer for more details). In this case, a memory barrier is required within the main thread to guarantee the main thread observes changes to finished_reading by other threads. Consider using either a Boost.Thread synchronization mechanism like boost::mutex, or Boost.Atomic's atomic objects or thread and signal fences.

Community
  • 1
  • 1
Tanner Sansbury
  • 51,153
  • 9
  • 112
  • 169
  • Thanks for your answer, it addresses all the issues of my example. The link to your answer on the behavior of io_service::run was very illustrative. This is just a test code, my actual code implements a timeout based on packet parsing details in that main threads scope in the spinning loop. If the data is coming in on a half duplex bus and is inherently synchronous (as long as nothing has gone wrong), is it reasonable to use a synchronous structure with the async read so I can make sure to timeout and clean up the mess from the failed read? – st3am Sep 29 '14 at 02:51
  • I think I understand what you are saying about re-examining the use of synchronous and asynchronous behavior. It seems like I should link the asynchronous calls to fill a buffer and then go read from it in a synchronous thread after sending a request. – st3am Sep 29 '14 at 21:37
  • @st3am If you need timeouts, then asynchronous operations are required. However, using an additional thread for this may just complicate the solution. Consider examining the timeout approaches used in the official Boost.Asio [timeout examples](http://www.boost.org/doc/libs/1_56_0/doc/html/boost_asio/examples/cpp03_examples.html#boost_asio.examples.cpp03_examples.timeouts). For a half duplex system, one can generally use a single thread to wait on either the read to finish or a timeout, then respond and repeat. – Tanner Sansbury Sep 30 '14 at 02:06
  • For anyone else trying read from a half duplex device on linux: I found out that the half duplex architecture ended at the ftdi chip I was reading from, at least for my case I didn't have to worry about avoiding reads when I was writing. Using all of Tanners suggestions I was able to read but bytes would occasionally be lost if the time between the read and write became large. I think this usually occurred from context switching, all bytes would be lost if I introduced a delay of a few ms between the write and read. Using a chain of async_read_some calls to fill a std::deque was the solution. – st3am Oct 01 '14 at 18:22
1

Note that boost::bind copies its arguments. If you want to pass an argument by reference, wrap it with boost::ref (or std::ref):

boost::bind(readCallback, boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred, boost::ref(finished_reading), ec, bytes_read));

(However, strictly speaking, there's a race condition on the bool variable you pass to another thread. A better solution would be to use std::atomic_bool.)

Igor R.
  • 14,716
  • 2
  • 49
  • 83
  • Thanks for your answer, the use of the ref wrapper allowed the callback parameters to be modified when the run event had not yet returned. – st3am Sep 29 '14 at 02:57