Live data from Hacker News

Asynchronous IO in Rust

medium.com

11–20 of 111 posts

Re: Asynchronous IO in Rust

#11
post #5
post #4

Earlier quoted context omitted.

> What's so wrong with Rust threads, excepting perhaps them being heavyweight? (Far better to solve that problem directly.) Nothing. If threads work fine, use them! That's what most Rust network apps do, and they work fine and run fast. A modern Linux kernel is very good at making 1:1 threading fast these days. > And having written network servers with pretty much every abstraction so much as mentioned in the article…

I'm down with all that. I'm really arguing in favor of threading here rather than any particular model of it. Plus, if you write threaded code, you can change the runtime out. Rust guarantees all the hard parts, anyhow. I wouldn't even be surprised once Rust settles down further it turns out there's some sort of hybrid solution that is better than either 1:1 or M:N on its own because it has superior insight into what…

Are you talking about threads-the-programming-model (vs. events, callbacks, channels, futures/promises, dependency graphs), or are you talking about threads-the-implementation-technique (vs. processes or various select()-like mechanisms)? And if you are talking about threads-the-programming-model, which synchronization mechanism: locks, monitors, channels/queues, transactional memory?

If the various threads in your application don't share data, then you don't need to worry about all the pitfalls of threading. But if they don't share data and you don't care about small (~10%) performance differences, why not use processes? Then you can use a really straightforward blocking I/O model, and you get full memory isolation & security provided by the OS.

The interesting questions happen when you a.) want to squeeze as much performance out of the machine as possible or b.) need to share data between concurrent activities. Then all of the different programming models have pros and cons, and I'm not sure you can define a "best" approach without knowing your particular problem. (Which, IMHO, validates Rust's "provide the barest primitives you can for the problem, and let libraries provide the abstractions until it becomes clear that one library is a clear winner" approach.)

FWIW, my experience in distributed systems is that threads+locks is a terrible model for writing robust systems, and that once you're operating at scale, you really want some sort of dependency-graph dataflow system where you specify what inputs are required for each bit of computation and then the system walks the graph as RPCs come back and input becomes available. This lets you attach all sorts of other information to nodes - timeouts, latency statistics, error statistics, whether or not this node is required and what defaults to substitute if it fails, tracing, logging, load-balancing, etc. It also adds a huge amount of cognitive overhead for someone who just wants to make a couple database queries. I wouldn't use this for prototyping a webapp, but it's also invaluable when you have a production system and ops people who need to be able to shut down a misbehaving service at a moment's notice while still keeping your overall product up.

Re: Asynchronous IO in Rust

#12
post #4
post #3

If you use threads, green or otherwise, you don't have to "implement" special code for composing things together, you get the full set of tools for composing code together, which includes, in passing, state machines, among all the other things it includes. This basically implements an Inner Platform Effect of an internal data-based language for concurrency that the language interprets, which will A: forever be weaker…

> What's so wrong with Rust threads, excepting perhaps them being heavyweight? (Far better to solve that problem directly.) Nothing. If threads work fine, use them! That's what most Rust network apps do, and they work fine and run fast. A modern Linux kernel is very good at making 1:1 threading fast these days. > And having written network servers with pretty much every abstraction so much as mentioned in the article…

Most of my work involves kernel programming (device drivers for hardware or virtualisation companies etc.); almost all of the rest of it is writing networking code (custom protocol implementations). So my reasons for evaluating[1] Rust are that I think C and C++ are pretty awful languages for writing code that's as security and reliability critical as the code I touch on a day to day basis, while memory-managed languages aren't even on the menu for me - kernel hacking is a fairly unpopular niche, and I'm used to it being ignored in terms of programming language and library design. My opinions/statements below are primarily centred around this set of use cases:

* There is little practical difference between regular threads and green threads in an OS kernel. Most OSes can't handle it if you manually mess with the stack in a kernel context anyway. (thread records are typically accessed by rounding the current stack pointer) So if you want to do synchronous-style I/O with real stacks, you'll need an OS thread for each task.

* Kernel thread stacks are fairly small; 8-16KiB are typical. The reason for this is of course that kernel stacks must use wired (non-pageable) memory, or interrupts will irrecoverably page fault. 8-16KiB is much too small for buffers of course, but also enormous compared to the actual amount of non-buffer state for most I/O tasks. In any case, you never ever want to get too close to utilising the theoretical maximum as you risk crashing the system. So for every kernel thread you create, you know you're wasting precious wired memory. Obviously, this is true for threads created from userspace too, but in the kernel, people usually don't have a choice about running your code, so you try to be as good a citizen as possible, and not fire off hundreds of threads.

* In many contexts, dynamic memory allocations are not reliable, and allocation failure must not impede progress. (I.e. I can't wait for the system to page some memory out to disk if my code is on the critical path for disk I/O.) So typically, it is desirable to pre-allocate enough memory to keep all the state required for a sequence of operations from start to finish. It's even less likely you can just spin up a new thread; so the threaded approach implies keeping a pool of threads around which is guaranteed to be big enough. A.k.a. a waste of resources.

* The chain-of-callbacks approach to I/O is even more awful in languages and environments with manually managed memory and resources.

* If I'm going to pick a fancy new language to write my drivers in, I'm still going to have to use the custom alloc/free functions for each type of kernel object (network packet, etc.) that the OS I'm writing against happens to use.

So typically, you end up either splitting your code into a bunch of callbacks, or you create a complicated explicit state machine with a giant dispatch switch() statement. In both cases all state tracked in a giant struct. Keeping track of control flow is tricky. Maintaining invariants is tricky. Making sure you don't leak (or over-free, or use-after-free) any of the resources you touch is tricky.

It'd be really, really nice if the language could help you out with this. Write it as synchronous code, and the compiler turns each location where execution can be suspended into a callback function. The locals that are used across suspension points are stored in an automatically generated struct (bonus points: unions for state which is guaranteed to not have overlapping lifetimes) which can be preallocated before firing off the "task" in question. As far as I'm aware, such a code transformation would effectively be a CPS-transform, (continuation passing style) which has at least been researched quite a bit in theory, if not so much in non-GC-language practice.

I haven't been able to invest large amounts of time into really learning Rust and applying it to my use case in earnest. It's tough without a remotely compliant standard C library around, and I've struggled a bit with trying to pick and choose bits out of Rust's 'core' library without bringing on an avalanche of dependencies. (any kernel module code is kept in wired memory, so unused code is a waste of resources in the kernel) I'm determined to overcome that though and use Rust for something other than a toy kernel module, and see if and how it improves things over C. If Rust does start supporting some kind of advanced I/O pattern that works in that sort of constrained environment, that seems like a significant competitive advantage.

[1] https://github.com/pmj/rustykext

Re: Asynchronous IO in Rust

#13
post #4

Earlier quoted context omitted.

> What's so wrong with Rust threads, excepting perhaps them being heavyweight? (Far better to solve that problem directly.) Nothing. If threads work fine, use them! That's what most Rust network apps do, and they work fine and run fast. A modern Linux kernel is very good at making 1:1 threading fast these days. > And having written network servers with pretty much every abstraction so much as mentioned in the article…

> Green threads didn't provide performance benefits over native threads in Rust. That's why they were removed. This seems wrong. This shouldn't be the point of green threads -- green threads aren't a "faster" alternative to native threads. How would that work? How could it be the case that virtual threads implemented in user space are faster than native threads provided by the OS? I don't think anyone expects that to…

Rust's green threading was... unusual. It attempted to abstract green and native threads into a single API, which lead to a lot of unnecessary overhead:

https://github.com/rust-lang/rfcs/blob/0806be4f282144cfcd55b...

Sadly, that proposal also removed concurrent IO from the standard library, and it hasn't been replaced.

Re: Asynchronous IO in Rust

#14
post #5

Earlier quoted context omitted.

I'm down with all that. I'm really arguing in favor of threading here rather than any particular model of it. Plus, if you write threaded code, you can change the runtime out. Rust guarantees all the hard parts, anyhow. I wouldn't even be surprised once Rust settles down further it turns out there's some sort of hybrid solution that is better than either 1:1 or M:N on its own because it has superior insight into what…

Are you talking about threads-the-programming-model (vs. events, callbacks, channels, futures/promises, dependency graphs), or are you talking about threads-the-implementation-technique (vs. processes or various select()-like mechanisms)? And if you are talking about threads-the-programming-model, which synchronization mechanism: locks, monitors, channels/queues, transactional memory? If the various threads in your a…

Do you have any links on the "dependency-graph dataflow system" that you are talking about? Sounds a little bit similar to what I'm trying to do, except at higher scale.

Re: Asynchronous IO in Rust

#15
post #3

If you use threads, green or otherwise, you don't have to "implement" special code for composing things together, you get the full set of tools for composing code together, which includes, in passing, state machines, among all the other things it includes. This basically implements an Inner Platform Effect of an internal data-based language for concurrency that the language interprets, which will A: forever be weaker…

One nice thing about Rust is how well suited it can be for embedded use. It is something like a smaller safer easier C++. For embedded you need to bound resources. Small network appliances are a good candidate for async IO. Many threads and stacks with a lot of dynamic allocation are not a good fit for a resource constrained device.

Re: Asynchronous IO in Rust

#16
post #4

Earlier quoted context omitted.

> What's so wrong with Rust threads, excepting perhaps them being heavyweight? (Far better to solve that problem directly.) Nothing. If threads work fine, use them! That's what most Rust network apps do, and they work fine and run fast. A modern Linux kernel is very good at making 1:1 threading fast these days. > And having written network servers with pretty much every abstraction so much as mentioned in the article…

Most of my work involves kernel programming (device drivers for hardware or virtualisation companies etc.); almost all of the rest of it is writing networking code (custom protocol implementations). So my reasons for evaluating[1] Rust are that I think C and C++ are pretty awful languages for writing code that's as security and reliability critical as the code I touch on a day to day basis, while memory-managed langu…

What do you think of C++ coroutines? Two competing standards proposals, already available as libraries:

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n445...

http://blogs.msdn.com/b/vcblog/archive/2014/11/12/resumable-...

Re: Asynchronous IO in Rust

#17
post #3

If you use threads, green or otherwise, you don't have to "implement" special code for composing things together, you get the full set of tools for composing code together, which includes, in passing, state machines, among all the other things it includes. This basically implements an Inner Platform Effect of an internal data-based language for concurrency that the language interprets, which will A: forever be weaker…

Well, I believe that it's almost impossible to make rust threads lightweight because every green thread needs a stack anyway. This may be fixed with some stuff like `async/await`. But let's talk about what's wrong with threads: 1. Timeout handling is ugly: you need to account a timeout in each read and write operation. At least timeout handling makes coroutine/threaded code no better than state machine code. But actu…

I've been writing in this model for nearly ten years now. In practice, what you cite as problems aren't.

1: In either approach, somewhere in your event loop you're setting yourself a timeout to fire. Haskell & Erlang do use exceptions, but Go does not, it simply makes this a first-class concern of the core event loop. This is only a problem in languages where the threading was bolted on after-the-fact. Which is a lot of languages, which matter because they have a lot of code. I don't mean to dismiss those real problems. But it's not a fundamental problem, only accidental.

2. In practice, this is not a problem I ever worry about. You get a DB library, it provides pools, unless you're talking to a very, very fast DB (like, memcached on localhost fast) this is one of those cases where IO really does dominate any minor price of thread scheduling.

3. This has been solved for a long time. Go has the nicest little catch phrase with "share memory by communicating instead of communicating by sharing memory", but each of Haskell, Erlang, and Go have their own quite distinct solutions to their problems, and in practice, all of them work. There's other solutions I merely haven't used, but I hear Clojure works, too. (Perhaps arguably a subset of the several approaches Haskell can use. Haskell kind of supports darned near everything, and you can use it all at once.)

This is part of why I write this sort of thing... at its usual glacial pace (despite how much we like to flatter ourselves that we move quickly), the programming community is finally getting around to being really seriously pissed off about how bad threading was in the 1990s. Good. We should be. It sucked. Let us never forget that. But what has not been so well noticed is that the problems with threading have basically been fixed, and in production for a long time now (i.e., not just in theory, but shipping systems; go ask Erlang how long it's been around). You just have to go use the solutions. Don't mistake debates about the minutia of 1:1 OS threading vs. M:N threading and which is single-digit percent points faster than the other for thinking that threading doesn't work.

Lest I sound too pollyannaish about what is still a hard domain, the way I like to put this is that threading has moved from an exponentially complex problem to a polynomially complex problem. (And Rust is leading the way on making even the polynomial have a small number in the exponent.) There's still a certain amount of complexity in making a threaded program go zoom, and it does require some adjustments to how you program, it's not "free", but rather than requiring wizards, it merely requires competent programmers who take a bit of care and use good tools and best practices now.

Re: Asynchronous IO in Rust

#18
post #4
post #3

If you use threads, green or otherwise, you don't have to "implement" special code for composing things together, you get the full set of tools for composing code together, which includes, in passing, state machines, among all the other things it includes. This basically implements an Inner Platform Effect of an internal data-based language for concurrency that the language interprets, which will A: forever be weaker…

> What's so wrong with Rust threads, excepting perhaps them being heavyweight? (Far better to solve that problem directly.) Nothing. If threads work fine, use them! That's what most Rust network apps do, and they work fine and run fast. A modern Linux kernel is very good at making 1:1 threading fast these days. > And having written network servers with pretty much every abstraction so much as mentioned in the article…

I'm not sure what you mean by the "syscall overhead", but I 100% agree that stackless coroutines in Rust would be amazing. But I'm also skeptical that they can be implemented as a library without compiler support. C++ only manages with some very unhygienic macros, switches, and gotos:

https://github.com/chriskohlhoff/asio/blob/master/asio/inclu...

Re: Asynchronous IO in Rust

#19
post #5

Earlier quoted context omitted.

I'm down with all that. I'm really arguing in favor of threading here rather than any particular model of it. Plus, if you write threaded code, you can change the runtime out. Rust guarantees all the hard parts, anyhow. I wouldn't even be surprised once Rust settles down further it turns out there's some sort of hybrid solution that is better than either 1:1 or M:N on its own because it has superior insight into what…

Are you talking about threads-the-programming-model (vs. events, callbacks, channels, futures/promises, dependency graphs), or are you talking about threads-the-implementation-technique (vs. processes or various select()-like mechanisms)? And if you are talking about threads-the-programming-model, which synchronization mechanism: locks, monitors, channels/queues, transactional memory? If the various threads in your a…

Threads+fine-grained locks sucks.

But it wasn't necessarily the threads, so much as the fine-grained locks.

I'm talking about threads-the-programming-model, where we don't throw away the gains we made with structured programming. I suppose I ought to get off my duff and finish the blog post that explains exactly what that means, but if you know what structured programming is, that's actually enough to explain what I mean. (It's just that nowadays most people don't actually know what it is, not because they have no familiarity with it, but because they have too much, and don't realize that it was not actually the "ground state" of programming but actually a hard-fought advance, which we are all collectively on the verge of spending the next, oh, 5-10 years painfully rediscovering the value of.)

Re: Asynchronous IO in Rust

#20
post #15
post #3

If you use threads, green or otherwise, you don't have to "implement" special code for composing things together, you get the full set of tools for composing code together, which includes, in passing, state machines, among all the other things it includes. This basically implements an Inner Platform Effect of an internal data-based language for concurrency that the language interprets, which will A: forever be weaker…

One nice thing about Rust is how well suited it can be for embedded use. It is something like a smaller safer easier C++. For embedded you need to bound resources. Small network appliances are a good candidate for async IO. Many threads and stacks with a lot of dynamic allocation are not a good fit for a resource constrained device.

There are still other blockers on embedded though. LLVM doesn't target every architecture, and still no allocator API or OOM handling.
Post reply on HN