"block_in_place" and blocking sleep, or async sleep

Howdy.

We're running some async task using Tokio and, having performed a write to a serial port for RS485, need to sleep until those characters are transmitted, plus a few ms more - generally around 10ms all-up.

Given a library we're using, this writer task is required to be async.

What might be the preferred approach to having a reasonably accurate sleep - should we use Tokio's block_in_place and std::thread::sleep, or just use Tokio's async sleep?

Here's the code in question with block_in_place, which is observed to work nicely:

// We block here as it is important that we wake up in time,
// and so we give ourselves the best chance.
tokio::task::block_in_place(|| {
    const BITS_PER_BYTE: u64 = 8;
    const START_BIT: u64 = 1;
    const STOP_BIT: u64 = 1;
    const BITS_PER_CHAR: u64 = BITS_PER_BYTE + START_BIT + STOP_BIT;

    let time_on_wire =
        Duration::from_millis(1000 * BITS_PER_CHAR * n as u64 / self.baud_rate as u64);
    std::thread::sleep(time_on_wire + self.enable_tx_flush_delay);
    self.disable_tx();
})

Thanks.

Tokio advises against relying on block_in_place. It's better than nothing, but can still cause problems.

In tokio, it's best to use tokio's async sleep, not std::thread.

If you use spawn_blocking instead, you'll get a real thread that you can block without problems, and use std::thread::sleep as much as you want.

Which approach is better also depends what are your timing and latency requirements. I presume exact timing may be important for serial communication. If you spawn a dedicated thread for it, you will have more control over the timing.
There may also be some lower-level APIs and libraries that could notify you when the data has been sent/received, which may be better than just waiting.

1 Like

You may wish to spawn a dedicated thread for interacting with your serial port. You could communicate with it from Tokio using tokio::sync::mpsc.

1 Like

Given a library we are using, spawning a blocking thread isn’t an option- unless we stop using the said library.

Asking the question a different way, which approach is going to provide a more accurate sleep?

And what exactly is the downside of blocking in place, if any, given four cores at my disposal…?

Using block_in_place will probably make your sleep slightly more accurate. The downside is that block_in_place is rather expensive because you're telling the runtime that it can no longer use the current thread to schedule any other async jobs, so Tokio will spawn a new thread to replace it. If you perform these sleep often, that's going to have a negative impact on other threads on the runtime because they're getting moved to new threads all the time.

1 Like

Tokio could spawn a new thread and it could happen that this new thread would run on the same core as a task in block_in_place, so other thread could be scheduled before that task. If you have several cores you could use two tokio runtimes - one on a single core for communication with rs485 (and use synchronous wait there - as there is no other task running concurrently, so you can allow pause this tokio runtime) and the other cores with the second tokio runtime. You could use channels to communicate between these two runtimes. This way one core will be dedicated only to the serial communication and won't be used for other tasks, so general performance could drop in case of a huge traffic.

1 Like

Spawning a new thread was lost on me re block_in_place. For some reason I thought tokio would somehow move pending tasks on the current thread to some other existing thread, and this would be inexpensive.

Thanks for the replies. I think I might ultimately dedicate a thread to the serial comms and do away with the async lib I’m using. Meanwhile, I’ll revert to the async sleep and observe the serial comms on a scope. The timing may still be fine.

If this were how it worked, then the effective size of the worker thread pool would shrink by 1 thread for every block_in_place(), reducing potential throughput.

1 Like

I would have expected it to use its thread pools. Move the current thread to the blocking pool and a spare blocking thread to the async worker pool. (Edited for clarity)

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.