Alternatives to async/await

I have a compute-heavy but interactive (via the sdl2 crate) workload that occasionally performs I/O (file and/or network reads and writes). The results of the I/O operation don't need to be immediately available -- it's possible (and desired) to run the I/O concurrently while the computation is ongoing. On the other hand, I/O is infrequent enough that the vast majority of cases, only one file read/network operation is going on at any given moment in time, if one is going on in the first place. I also return periodically to poll and service SDL window events, making my crate look something like this:

fn main() {
    create_sdl2_window();
    loop {
        check_sdl2_events();
        repaint_sdl2_window();
        check_io_completion(); // this is where we check if the download is complete
        for i in 0..1000000 {
            // do something for a finite length of time
            if data_from_network_needed {
                if !download_complete {
                    add_to_download_queue("https://users.rust-lang.org/");
                    // immediately continue computation, allowing the download to progress in the background
                } else {
                    // copy data into internal structures
                }
                // we can also open files
            }
        }
    }
}

I would like to avoid using async/.await; the main reason is that it would require labeling a large number of functions, some of which are performance critical, with async. I would rather use blocking I/O, but that would unfortunately lead to lag and freezes when the network is slow. The actual computation is highly serial by its nature and cannot be split into multiple threads.

My ideal solution would be to have one thread for computation (let's call this A) and one for I/O (B) -- B would sleep most of the time, only waking when A requests/sends data. A would poll B from time to time using the check_io_completion function to check whether the I/O's done or not. I'm fine with (and would prefer, due to their simplicity) blocking I/O crates like ureq or the std::fs::Read, so long as the operations run on a separate thread.

Do you know of any crates/blog posts/ideas that could help point me in the right direction?

You can send I/O operations onto a thread with channels, and then return the data on a different channel.

use std::sync::mpsc;
use std::thread;

pub fn main() {
    placeholder::init();
    let (
        job_sender, job_receiver
    ) = mpsc::channel::<Box<dyn FnOnce() -> Vec<u8> + Send>>();
    // some way to return the downloaded data to thread A
    // Vec<u8> probably isn’t the *best* type, but this is an example
    let (data_sender, data_receiver) = mpsc::channel::<Vec<u8>>();
    // spawn thread B
    let io_thread = thread::spawn(move || {
        while let Ok(job) = job_receiver.recv() {
            let data = job();
            let _ = data_sender.send(data);
        }
    });
    loop {
        placeholder::before_io();
        if placeholder::need_some_data() {
            let data = match data_receiver.try_recv() {
                Ok(d) => d,
                // thread B has no data available
                Err(mpsc::TryRecvError::Empty) => {
                    if placeholder::not_already_downloading() {
                        job_sender
                            .send(Box::new(|| placeholder::download()))
                            .expect("thread B should exist");
                    } else {
                        // download is still in progress, do something
                    }
                    // exit this loop iteration, no data is available
                    continue;
                }
                Err(mpsc::TryRecvError::Disconnected) => {
                    panic!("thread B should exist");
                }
            };
            placeholder::do_something_with(data);
        }
    }
    // This code below is only necessary if the main loop ever exits in your program

    // Make sure thread B will exit shortly
    drop(job_sender);
    io_thread.join().unwrap();
}

Playground

If synchronizing which I/O result is getting sent back over the data_receiver is too hard, you can send over the job channel an enum Job, with variants for each I/O operation you want to do, and a response_channel containing a oneshot channel to send the response back.

1 Like