6 ms·
I was a huge opponent of Rust, but finally decided to give it a try in anger once again. I began writing a large application, and noticed that many of my libra
by sargun 3y ago
I was a huge opponent of Rust, but finally decided to give it a try in anger once again.
I began writing a large application, and noticed that many of my libraries only offered async versions, and the promise was quite appealing -- not having to worry about threads or concurrency as long as I followed certain rules. What I ended up with was an incredibly slow application because of the limitations of Rust async, and the runtime. All of my I/O got pushed through one thread (with tokio), and that, plus scheduling overhead became my bottleneck. Debugging this was a nightmare in writing my own tracing tools.
20/20 hindsight, I would not write my code to be async, and would just prefer threads. I'm really not sure how / why async took off the way it did.
- insanitybit 3y agoIO shouldn't be going to one thread, as far as I know. Blocking IO would go to a threadpool. But you can just `block_on` your futures if you want and not think about it at all.
- cmollis 3y ago+1
- jackhalford 3y agoasync took off when multicore processors came out and C had no first class way of running in parallel, so we bolted on threading libraries that are second class. Take a look at zig’s approach to concurrency, it’s so first class you can write your own event loop without an OS, i.e you could use the language’s async to write an OS.
- Sharlin 3y agoAsync doesn’t really have much to do with multicore. Indeed the most used async environment on the planet (JavaScript) is (used to be) strictly single-threaded. Async is all about keeping the CPU busy even though the stuff it does involves latencies thousands or millions of times longer than CPU timescales – and about abstractions that allow you to pretend you’re writing normal synchronous code when the reality is anything but. This mostly just involves chopping up the code and turning it into a state machine. Async is almost useless when you’re not incredibly I/O bound. But many people these days are because the web ate the world. C did and does have a poor almost-anything story but it doesn’t and didn’t really matter because C hasn’t been relevant on the web since 1995 or so.
- jcranmer 3y ago> Async is almost useless when you’re not incredibly I/O bound. I'm not sure that's true. The async/await model is about representing a state machine in imperative code. Not all state machines can be written imperatively, but when they can be, it is often clearer than writing the state machine manually. I/O is the most common scenario for such state machines, but I can see a few other OS scenarios where you might want to use async/await instead (e.g., process management is probably better represented with async/await).
- jackhalford 3y ago> async doesn’t have much to do with mumticore It does, once you have async you can multiplex n coroutines on m cpu cores, meaning you can throw libthread out of the window. > C hasn’t been relevant on the web since 1995 Do you know nginx, a core web technology is written in C? Linux is also C. Don’t forget that at the end ov every web request are syscalls and hardware.
- nextaccountic 3y agoWhy didn't you use the multithreaded executor from Tokio?
- tuetuopay 3y agoTokio does sync I/O in the background with dedicated I/O threads if memory serves. (it is sync in the sense it does syscalls, which are pushed to a dedicated thread).
- dymk 3y agoDepends on the type of I/O (network, file, etc), and the async abstractions that the target operating system supplies.
- littlestymaar 3y agoOnly for file access, which wasn't supported by async on Linux until io-uring.
- nextaccountic 3y agoNo, any spawn_blocking will run on its own dedicated thread
- littlestymaar 3y agoI missread the comment above and read `async` instead of `sync`, my bad. (Your response isn't entirely accurate though: it's using a thread pool whose size is configurable, not one thread per spaw_blocking)
- ReactiveJelly 3y agoEven then I'm pretty sure it's not _a_ dedicated thread, but a thread pool that defaults to a maximum of 512 blocking threads. There's no reason it would bottleneck unrelated writes on unrelated Files through the same thread. I tried to dig into the code here https://news.ycombinator.com/item?id=38180860 https://news.ycombinator.com/item?id=38180860
- gnulinux 3y agoIn these debates I find myself very confused. I feel very comfortable with async ergonomics in Rust. It seems like the argument against it is that it has poor ergonomic... but how? It's rather straightforward to go from sync to async and async to sync you just have to code the strategy in. If you're in sync and need to run async, you need to run it in some executor (either in threads or in single-thread concurrency or something else). I've been writing async code in Python for years I'm truly missing what's so bad -- or even different -- about Rust async. What am I missing? I'll continue to write async code because it's pretty nice. EDIT: People talk about swapping executors etc but it seems like 99.999% of the programming applications you don't need anything like this, nor does something like Python supports this anyway and we're all fine with it.
- mplanchard 3y agoJust noting for the sake of countering this narrative that async rust is fundamentally broken that I feel the same way as you, having been working with async rust professionally for 2.5 years.
- rstuart4133 3y ago> It seems like the argument against it is that it has poor ergonomic... but how? It depends on what you're comparing it against. If you are comparing Rust async against other languages, then the major difference is Rusts borrow checker. In every implementation of async, you effectively move whatever state is needed off the stack and into a separately allocated memory area. In Rust that separate area is a closure. This interacts badly with Rusts borrow checker. The borrow checker needs to know the lifetime of any object you deal with. It has two "base truths", by which I mean life times it already knows about that you can derive other life times from: static and the stack. If they don't suffice you have to handle life time management yourself and at run time using Rc or Arc or something. Being forced to do that complicates your types and slows the code down. The root cause is async in Rust removes one of those two base truths: the stack. So now you are forced to write that ugly manual life time management code far more often. This is unique to Rust. Every other language I know of that implements async has garbage collection, so while it remains true they also move stuff off the stack it doesn't change anything. You still use the same types, and apart from sprinkling async's and await's here and there and indenting your closures, your code remains the same. This is also why I think green threads are a much better fit for Rust than async. Under the hood green threads and async are very similar: they are both ways of doing event driven I/O. Their performance characteristics are near identical. The main difference is while async forces you to move your state to a different area, green threads you do it as before and store it on the stack, just like normal code. In fact green thread code looks identical to normal code. The only change you have to make to convert some code to green threads is change the name of the I/O calls to use non-blocking versions (which is something you also have to do with async, of course). But since the stack is still available all those fights with the borrow checker async creates go away, as does all the extra syntax async requires. It doesn't come for free of course, so the run time performance of green threads and async is not absolutely identical. In async every task shares the one stack, whereas in green threads they each get a new one. This creates some extra memory overhead, chews up considerable address space (which normally isn't backed by memory) in order to protect against stack overflow, and it costs a bit more to set up a stack. But once a task is setup green threads are going to be bit faster you aren't moving stuff and and off the stack and you don't get hit with those additional run time life time checks you were forced to introduce for async. Mitigating green threads overheads somewhat, a process that is handling 1000's of concurrently connections is unlikely to be running one a machine that is memory constrained, so the extra memory probably doesn't matter. (In reality a Raspberry Pi with 4GB of memory can handle 1000's of stacks.) And it's likely to be a 64 bit machine where address space is nearly free, so the "considerable extra address" space also doesn't matter. Still, I can think of once place it does matter. You can have two styles of generators: ones take the async approach and ones that take the green thread approach (ie, allocate an extra stack to each generator). Rust nightly does have generators and they currently take the green thread approach(!). But, generators tend to be short lived. You tend to use them to iterate over an array, rather than serving a web request (the typical use a task is put to). That means the overhead of creating the stack for a green thread isn't a small proportion of the time a long running task takes, but could well dominate the time it takes to iterate over a small array. Thus async is a much better fit for iterators. Currently Rust has this arse about: it has async for long running tasks, and green threads for generators in nightly. With this announcement it looks like this will be 1/2 fixed. Great!
- andrewstuart 3y agoIn the words of Steve Jobs, “you’re holding it wrong”. I’m guessing your code is blocking. You absolutely cannot do blocking code in async in any language, unless the blocking is super quick. No blocking io. async code should yield great performance if you are doing everything the right way. Yes it is single threaded but either run multiple threads in rust or run multiple instances of your program with systemd. That applies same to rust, Python, javascript, any async language. I wrote a prototype message queue in Rust with Actix and got 7 million messages per second via http.
- bad_user 3y agoJava, C#, probably Go and others, are able to do async I/O on multiple threads. It doesn't help with the actual async I/O being performed, but it does help with parallelising CPU work, or with the fact that not all I/O can be async and the runtime is faking it, such as local file I/O, which is going to be blocking system threads. Async I/O is all about multiplexing work on few CPU cores efficiently, and multi-threading is still required. Python or JavaScript are seriously limiting environments and should not be given as an example when we're talking about a language that does 1:1 scheduling.
- andrewstuart 3y agoI don’t really get your point. You cannot, (should not), do blocking IO in async in any language. The language provides libraries to do non blocking IO. What is unclear about that? Multi threading has nothing to do with async, except as a way to run certain things in an executor thread to avoid blocking. Also I don’t really know what you mean by async IO…. even in the languages you reference I would guess IO operations are all run in a single thread.
- bad_user 3y ago> You cannot, (should not), do blocking IO in async in any language. On platforms with 1:1 scheduling, of course you can. Blocking I/O executed on another thread, with a callback to execute when done, becomes async I/O (from the user's PoV). Ofc, when we talk about async I/O, we also refer to the kernel APIs being used, such as select/poll/epoll/io_uring. Say, working with Epoll is usually done via a single-threaded "event loop", but that only listens for the operations possible for an open file/socket. The read/write operations are still potentially blocking, so for efficiency you need multiple threads. The dirty secret is that async I/O, as implemented by Linux, isn't actually fully async.
- jokethrowaway 3y agoThat doesn't sound right. In which way was your application slow? Conceptually, async is not necessarily giving you parallelism but concurrency. Tokio (multithreaded) or async-std spawn N threads though and schedule your work for you. It would be interesting to see the code or know which libraries you used (or even just what type of application you were building). I built a heavy app using async-std and then rewrote it to threads just to better control what was each thread doing. Without my additional scheduling rules (which brought a lot of other benefits and helped performance in other ways), the performance between async and not async was close, with threads being marginally faster.
- sapiogram 3y agoCould you be more specific about your bottlenecks? Were you not able use the multithreaded scheduler for some reason, or did it not work properly?
- ReactiveJelly 3y ago> All of my I/O got pushed through one thread (with tokio) In all my use of Tokio in the last few years, I never heard of such a thing. In tokio::fs::File [1] it calls spawn_mandatory_blocking to do file writes, I assume this is similar to spawn_blocking [2] which sends a task to Tokio's blocking thread pool. That thread pool is supposed to max out at 512 blocking threads [3], unrelated to CPU core count. Tokio's TcpStream appears to be built on mio's TcpStream. I didn't dig deep into the code for this, but it doesn't just call spawn_blocking, and I'm assuming on Unix it ultimately registers the socket with epoll or equivalent, so it never blocks a thread to do socket reads or writes. Could you share more about your application? [1] https://docs.rs/tokio/latest/src/tokio/fs/file.rs.html#682 https://docs.rs/tokio/latest/src/tokio/fs/file.rs.html#682 [2] https://docs.rs/tokio/latest/tokio/task/fn.spawn_blocking.html https://docs.rs/tokio/latest/tokio/task/fn.spawn_blocking.ht... [3] https://docs.rs/tokio/latest/tokio/runtime/struct.Builder.html#method.max_blocking_threads https://docs.rs/tokio/latest/tokio/runtime/struct.Builder.ht...
- sargun 3y agoSo, it wasn't actually synchonrizing all of the I/Os onto one thread, but I wasn't getting any parallelism due to the amount of time required to dispatch I/Os. Essentially, my program was highly concurrent, but I wasn't able to get any I/O parallelism because each syscall was pretty cheap, and the cost of dispatching each I/O was too expensive.
- speed_spread 3y agoRust Async has very real constraints but it's definitely _not_ supposed to make your app slow. Most will agree that it is often best avoided unless demonstrated to be required by performance requirements. IMO this advice also applies to other lang's async, but even moreso in Rust. The complexity is real.
- pornel 3y agoCloudflare pipes a large fraction of the Web through tokio. The runtime has a very low overhead, and can scale to over a hundred cores.