How can I reliably clean up Rust threads performing blocking IO?

2.1k views Asked by At

It seems to be a common idiom in Rust to spawn off a thread for blocking IO so you can use non-blocking channels:

use std::sync::mpsc::channel;
use std::thread;
use std::net::TcpListener;

fn main() {
    let (accept_tx, accept_rx) = channel();

    let listener_thread = thread::spawn(move || {
        let listener = TcpListener::bind(":::0").unwrap();
        for client in listener.incoming() {
            if let Err(_) = accept_tx.send(client.unwrap()) {
                break;
            }
        }
    });
}

The problem is, rejoining threads like this depends on the spawned thread "realizing" that the receiving end of the channel has been dropped (i.e., calling send(..) returns Err(_)):

drop(accept_rx);
listener_thread.join(); // blocks until listener thread reaches accept_tx.send(..)

You can make dummy connections for TcpListeners, and shutdown TcpStreams via a clone, but these seem like really hacky ways to clean up such threads, and as it stands, I don't even know of a hack to trigger a thread blocking on a read from stdin to join.

How can I clean up threads like these, or is my architecture just wrong?

1

There are 1 answers

0
SpamapS On

One simply cannot safely cancel a thread reliably in Windows or Linux/Unix/POSIX, so it isn't available in the Rust standard library.

Here is an internals discussion about it.

There are a lot of unknowns that come from cancelling threads forcibly. It can get really messy. Beyond that, the combination of threads and blocking I/O will always face this issue: you need every blocking I/O call to have timeouts for it to even have a chance of being interruptible reliably. If one can't write async code, one needs to either use processes (which have a defined boundary and can be ended by the OS forcibly, but obviously come with heavier weight and data sharing challenges) or non-blocking I/O which will land your thread back in an event loop that is interruptible.

mio is available for async code. Tokio is a higher level crate based on mio which makes writing non-blocking async code even more straight forward.