Understanding Tokio readiness

I'm having trouble understanding how Tokio's readiness-driven, I/O works. I'm studying the echo UDP sample.

I put some debug printlns in the example to print out the flow of events. This is a record of it running, annotated with the "conversation" I was having with it via "nc":

# Initial startup, waiting for me to send it a datagram
Listening on: 127.0.0.1:8080
poll()
  loop
    about to call recv_from()

# After sending it a datagram, via the `nc(1)` utility
poll()
  loop
    about to call recv_from()
    recv_from: Some((1, V4(127.0.0.1:65500)))
  loop
    Echoed 1/1 bytes to 127.0.0.1:65500
    about to call recv_from()

# A second message, again sent from `nc(1)`
poll()
  loop
    about to call recv_from()
    recv_from: Some((22, V4(127.0.0.1:65500)))
  loop
    Echoed 22/22 bytes to 127.0.0.1:65500
    about to call recv_from()

My confusion lies at the last part of each of these three stanzas. The code seems to stop near the end of the poll() method, specifically at the call to recv_from() while waiting for input.

On one hand, recv_from() appears to block, because the println that comes immediately after doesn't get invoked. But when data does arrive, the poll() method is invoked anew. So the poll() function actually appears to have completed. This seems to make more sense, because all the Tokio funcitons are supposed to be non-blocking. But, I really don't understand what happens when recv_from() is called and there is nothing to read.

Some other, more general, questions are: what entity invokes the poll() method and what informs that entity that poll() should be called? Seems like understanding that is another key to making use of futures.

Thanks,

Chuck

6 Likes

The key is the try_nb! macro around the call to recv_from. This behaves similarly to the standard library try! macro, but as well as returning on error cases it also returns if the value indicates that it will block.

So, in this case, if recv_from returns a value (wrapped in Ok(Async::Ready(..)) the Server::poll function will carry on running and loop around, if there was no value to receive (or an error occurred) try_nb! will return out of the Server::poll function, returning the Ok(Async::NotReady) (or Err(..)) value to the caller. In this case it returns directly to the Task as the Server is the top level future that was spawned onto the event loop. The Task will then be parked by the event loop and wait for a notification that there is something more for it to do. When it gets that notification it is unparked and the Server::poll function will be called again.

Generally you don't need to worry about the notifications for unparking Tasks, that's a very low level part of how tokio works and is taken care of by tokio's IO wrappers. In this case at some point during recv_from when it determines that there is no UDP packet available the tokio_core::net::UdpSocket will use task::park to get the current Task and setup the Task::unpark method to be called when mio indicates there's new data available on the socket.

7 Likes

recv_from does not block. Instead, if there is no data available, it returns WouldBlock. This causes try_nb! to return from the function, which is why the next println! isn't reached.

The entity that invokes the poll method is the event loop, which is the Core type in this example. The event loop calls poll when the operating system/kernel informs it of I/O events, through mechanisms such as epoll.

5 Likes

Ah, thanks for the answers.

I'd read through the try_nb docs and source and noted its return values, but it didn't penetrate that try_nb is a macro, not a function. It ends up inlined into poll(), so its return statements cause a return from poll(), not back to poll(). That clears up the mystery.

As for requesting future calls from the event loop, sounds it's the call to recv_from() itself that does this. It returns NotReady to its caller, but behind the scenes, parks the task and tells the event loop to unpark and call back when data arrives.

2 Likes