Async /await or thread for pulling data from SerialPort

I have a library I'm working on that pulls data from the serial port using the SerialPort crate. SerialPort blocks the thread while waiting for new data, so I started by running it on a separate thread. This worked well until we did some refactoring where I needed to call a method on self with the pulled data from within the spawned thread. This caused all sorts of issues which ended up with me wrapping a reference to self in an Arc<Mutex> but I hit a wall with the lifetime checker (still learning Rust!).

I then refactored to use mpsc channels, where the IO thread sends it's data to the main thread for processing. This works, but it now blocks the main thread, I assume because of the tight loop where it calls try_recv:

    fn io_begin(&mut self)
    {
        match self.serial_port.take() {
            Some(mut serial_port) => {

                let (data_tx, data_rx) = mpsc::channel();

                let builder = thread::Builder::new()
                    .name("serial_port".into());

                let port_name = self.port_name.clone();
                let baud_rate = self.baud_rate;

                builder.spawn(move || {

                    let mut serial_buf: Vec<u8> = vec![0; 2048];
                    println!("Receiving data on {} at {} baud:", port_name, baud_rate);
                    loop {
                        match serial_port.read(serial_buf.as_mut_slice()) {
                            Ok(bytes_read) => {

                                data_tx.send(serial_buf.as_mut_slice()[0..bytes_read].to_vec());
                            },
                            // TODO log errors/call error callback
                            Err(ref e) if e.kind() == io::ErrorKind::TimedOut => (),
                            Err(e) => eprintln!("{:?}", e),
                        }
                    }
                }).unwrap();

                loop { // TODO This is currently blocking the UI thread! 
                    let received_data = data_rx.try_recv();
                    if received_data.is_ok() {
                        self.connection_struct.process_received_data(received_data.unwrap());
                    }
                }
            }
            None => {
                println!("port not opened");
            }
        }
    

So looking into async / await and Futures, I wonder if I could wrap the call to serial_port.read in a Future or async fn and call await on it, potentially negating the need to spawn a thread at all?

Or alternatively, build a non-blocking timer that awaits until new data is available from the thread rather than spinning on try_recv?

This is not really possible. The function is blocking and there isn't much you can do about that. Perhaps tokio-serial would be able to replace it?

There is a method called blocking which is similar to starting a thread pool, except that it is managed by Tokio, but I don't know if it still exists in Tokio 0.2

Thanks, but I'd like to avoid using tokio, as it seems overcomplex for the needs of our project.

If you need any advice in working with your thread based model, feel free to ask.

Yes please! I'll change the title accordingly.

The issue with the thread based model is the need to call a mut method on self either from within the thread (which requires a mutex due to one of it's members not being Send) or as I have in my example, using channels to send the data when its ready. What I want to be able to do is poll with try_recv without blocking the thread, possibly using a non-blocking timer.

I don't understand why its currently blocking as I assumed try_recv should not block, but perhaps the tight spinning loop is what's blocking the thread.

After switching to Arc<Mutex> and now back to channels, I am still confused as to why the loop with try_recv is blocking. Anyone able to shed some light on this?

Have you tried sleeping for a 100ms or something if there’s no new data from the try_recv call? Or does that pause other work on that thread. I’m assuming you’re maxing our a core with a loop like that.

I already tried sleeping, but it has the same effect - pauses other work on the thread. Maybe I need to split threads up in a different way... I'm trying to avoid using an Arc<Mutex>> which kinda works but ends up messy (and in my case with some deadlock issues)

Is that the actual code? As in the other work on the thread is the work done inside the function call in if statement?

Yes that is the code. The spawned thread reads data, sends it to the main thread which calls self.connection_struct.process_received_data(data) whenever there is new data.

when you tried sleeping, did you put it in the else condition of that conditional and use a short amount of time like 10ms?

Also when you say the thread is blocking, what exactly do you mean by that? Does new data coming in not propagate to the main thread immediately? Or is there some interactive component to the program that doesn’t respond? If it is what I was thinking, the data should still show up on the main thread as soon as it’s ready. It would just waste a lot of cpu to get that result.

No, the UI of the program (which is in C++) gets blocked, showing the spinning wheel on mac.

Not sure how this should be redesigned to not kill the CPU. All the examples I found have try_recv in a tight loop like that.

And no I didn't sleep in the else, just in the loop. 50ms. Trying in the else now..

Update: no difference.

What code operates the ui? What libraries are you using for the ui? I’m not very familiar with creating ui’s but I think some platforms have requirements that the main thread must be available to respond to events. If that’s the case, you wouldn’t want to sleep the main thread, or operate it at 100% usage which is what your code looks like it’s doing. But I’d probably need someone else to chime in since I don’t have experience making ui’s.

I'm using JUCE to create the UI. I think your assumptions are correct. I need to find a way to offload the whole thing to a different thread, not just the data receiving part. I'm going to dig a bit and see if I can refactor things to allow me to move the connection_struct into the spawned thread.

Did you find a solution to this problem @adamski? :slight_smile: I'm currently experiencing a similar issue and wondered how you went about solving it, if you managed to.