arisuchan.xyz arisu
[ sci / cult / art ]   [ λ / Δ ]   [ psy ]   [ random ]   [ meta ]   [ all ]   [ irc / radio / tv ]
[ sci / cult / art ] [ λ / Δ ] [ psy ] [ r ] [ q ] [ all ]
Arisu Theme Lain Theme


/λ/ - programming

Striped sock enthusiasts
Name
Email
Subject
CommentFormatting
Captcha
File
Embed
Password (For file deletion.)

File: 1773631212775.png ( 9.86 KB , 900x673 , png-clipart-rust-programmi….png ) ImgOps

 No.29

Most “modern” async solutions these days come bundled with thread pools, executors, reactors, hundreds of macros, and half a megabyte of generated state machines just to read 4kb from a socket.
I’m trying to keep a personal networking tool extremely lean. Think single binary < 5mb, no heavy runtime if possible. But I still want proper non-blocking I/O without busy-looping or blocking the whole program.
Heres the kind of "ugly-but-works" code I usually end up writing when I want to stay close to the metal:
Rustuse std::io::{self, Read, Write};
use std::net::TcpStream;
use std::os::unix::io::AsRawFd;

fn main() -> io::Result<()> {
    let mut stream = TcpStream::connect("1.1.1.1:443")?;
    stream.set_nonblocking(true)?;

    let mut buf = vec![0u8; 4096];

    loop {
        match stream.read(&mut buf) {
            Ok(0) => break,                     //eof
            Ok(n)  => { /* handle data */ }
            Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
                //this is the part that usually turns into mio / epoll / kqueue / iocp / select spaghetti
            }
            Err(e) => return Err(e),
        }
    }
    Ok(())
}

The question is: how do you people actually handle the “wait for something to happen” part in 2026 without immediately reaching for tokio/async-std/libuv/go runtime/whatever-the-800-line-crate-of-the-week-is?

 No.30

It's very simple to make an async scheduler without macros and craziness if:
- You are single-threaded
- You only target one OS
- You don't need accurate timers

I've built an async scheduler in Nim from scratch using Linux syscalls, and it was easy until I ran into threads and timers.

For threads, what you can (and should) do if you are writing a network tool is to do a shared-nothing architecture with a scheduler per-thread. This means that as long as you aren't passing data between threads, you do not need atomics or locks. This eliminates what people usually complain about with async Rust, using Arc<T> everywhere.

Before I talk about why timers are hard, let's look at a conceptual single-thread async scheduler:

Task - in Rust terms, this will be a Future
Queue - Vec<Future>
Loop - While queue is not empty, pop a task and poll it. If it's done, discard it, if not, put it back on the queue.

That's all that's required to build a simple async scheduler, fundamentally. The real question is, how often do you run the loop body? This is how you get into waking up. You'll usually tell epoll on Linux to put you to sleep until something is available to read. This could be some network data or whatever. So in the simplest terms, that's how you do it.

Now, as far as timers go: how often do you wake up? You could just make a Future for timers and only poll success when the time it needs to wake up has passed. This is suboptimal because you're basically running at 100% CPU all the time. So, your other option is to have the kernel wake your thread up after some amount of time.

In Linux, you can create a timer that will wake your thread up when it fires, but each one of those timers has a file descriptor assigned to it, the same thing used to track open files and sockets. You could create one of these for every sleep() call, but you would quickly run out of file descriptors. On Linux, processes can only have around 1,000 file descriptors open at once.

To avoid the file descriptor problem, you need to choose your minimum timer resolution, then create buckets for tasks that are checked based on timer resolution. That way, you can have only a few kernel timers that wake up and check buckets of userland timers based on the resolution.

The above is how things like Tokio (and presumably Go) work.

With all that said… if you have a "simple" and "minimal" network tool, you can probably just get away with using threads. You probably don't even need async. Writing a simple network program using TCP sockets and threads on Linux is very simple in C and Rust and the performance will probably be just fine.

 No.31

>>30
By the way, all this assumes you're running Linux with epoll. If you are using anything else, especially Windows, it will be very different. Windows (and the semi-new io_uring Linux module) use a completion model (you call the kernel, it lets you know when it's done) vs. a readiness model (it lets you know when you can submit work).

I highly recommend anyone who is interested in high-performance Linux networking to read about io_uring. There is a great introduction to it here: https://unixism.net/loti/



[Return][Go to top] Catalog [Post a Reply]
[ sci / cult / art ] [ λ / Δ ] [ psy ] [ r ] [ q ] [ all ]