[HN Gopher] Async Rust: What is a runtime? Here is how tokio wor...
       ___________________________________________________________________
        
       Async Rust: What is a runtime? Here is how tokio works under the
       hood
        
       Author : sylvain_kerkour
       Score  : 155 points
       Date   : 2022-07-16 15:22 UTC (7 hours ago)
        
 (HTM) web link (kerkour.com)
 (TXT) w3m dump (kerkour.com)
        
       | [deleted]
        
       | redman25 wrote:
       | Is it possible to build a tokio compatible library that might not
       | be so heavy weight? Maybe this a moot point since libraries would
       | use tokio as a dependency anyway.
        
         | tempest_ wrote:
         | Smol is essentially that iirc.
         | 
         | https://github.com/smol-rs/smol
         | 
         | Though I have not used it extensively.
        
       | Linda703 wrote:
        
       | pkulak wrote:
       | People crap on async Rust because it's not the most graceful to
       | use, but I think it's kinda genius how they've managed to make it
       | zero-overhead. To the point that even the stack size of green
       | "threads" is known ahead of time.
       | 
       | The only issue I have is that it's tough not to use it. The big
       | HTTP libraries let you opt out, but smaller libraries don't have
       | the resources to do everything twice. I don't know what the
       | solution is, but it would be nice to always be able to chose.
       | It's pretty silly to use async networking in a cli app, for
       | example, but sometimes you have to.
        
         | CryZe wrote:
         | You can always just do let no_async =
         | block_on(something_async);
        
           | kangalioo wrote:
           | I'd instinctively worry about overhead there
           | 
           | Are there benchmarks comparing sync http libraries with
           | block_on-wrapped async http libraries?
        
             | bongobingo1 wrote:
             | Is the overhead going to matter if your content to block
             | anyway? Surely any threading etc constructs are tiny
             | compared to wire time, etc.
        
               | ibraheemdev wrote:
               | Blocking I/O+threads can actually scale very well now,
               | and with block_on you get the worst of both worlds, but
               | yeah, I agree that most people are probably fine with it.
        
               | vbezhenar wrote:
               | Most people fine with python. Those who come to Rust are
               | not fine with overhead.
        
               | dagmx wrote:
               | Some people who come to rest may not be fine with it.
               | 
               | A lot of people come to rust for other reasons, like
               | compile time safety checks etc...
               | 
               | But most importantly, the delta between the performance
               | of block_on and not, versus block_on and Python are
               | massively different. You can write inefficient rust and
               | still have a huge win over Python.
        
               | mcronce wrote:
               | This assumes that people only use Rust for the
               | performance. I don't think that's strictly true.
               | 
               | 95% of what I write isn't performance-critical or even,
               | really, performance-relevant. I still choose Rust for the
               | vast majority of projects for ergonomic and correctness
               | reasons.
        
               | adwn wrote:
               | You're spot on. I recently chose Rust over Python for a
               | very small program which reads a JSON (or Hjson) file,
               | does some checking and processing, and writes the results
               | to a different JSON file, because Rust has serde, proper
               | static type checking, algebraic data types, and other
               | features that made it more productive than Python (!!!)
               | for that specific use case. Performance wasn't even a
               | consideration, I made judicious use of _clone()_ and run
               | it in debug mode.
        
               | dagmx wrote:
               | Serde is such an awesome library. Having to decode
               | serialized data in the other languages (Swift, C++,
               | Python) I write is such a bear after using serde.
               | 
               | Swift comes closest with codable/decodable but it's often
               | still lacking the ergonomics of serde, especially the
               | attribute options per field
        
             | ibraheemdev wrote:
             | > I'd instinctively worry about overhead there
             | 
             | Yeah, one of the nice things about blocking I/O is that you
             | can perform it with a single syscall. With
             | block_on(async_io), you're now dealing with registration
             | with a reactor, polling epoll, and extra syscalls for each
             | I/O operation. Not to mention the overhead of running the
             | state-machine as opposed to line by line.
        
         | duped wrote:
         | > To the point that even the stack size of green "threads" is
         | known ahead of time.
         | 
         | This is only possible by not having stacks for those "green
         | threads" (they're stackless coroutines, not stackful
         | fibers/proper green threads).
         | 
         | It puts a severe limitation on the usefulness and forces any
         | kind of recursive coroutine to heap allocate on returns.
        
           | JoshTriplett wrote:
           | One other obvious alternative would have been to make _all_
           | futures heap-allocate so that _some_ can be recursive.
           | 
           | I think making recursive futures invoke `Box` or similar
           | seems consistent with Rust's general "only pay for what you
           | actually use".
        
             | duped wrote:
             | I disagree, because you're paying for boxing on return +
             | dynamic dispatch when it's perfectly safe/sound to have a
             | stackful coroutine that only allocates when the stack needs
             | to grow and doesn't require dynamic dispatch. So you _don
             | 't_ pay for heap allocated futures if they aren't
             | necessary, and you pay _less_ when they are.
             | 
             | So you're actually paying a higher price than if the
             | compiler could support stackful generators for recursive
             | futures. In fact async just gets a lot easier to write and
             | use if generators could be stackful, at least imho.
             | Generators don't have the syntax nice-ness of async/await,
             | but they're also more explicit which feels more in with the
             | rest of Rust, where syntax sugar like async/.await() is the
             | exception and not the norm.
        
               | JoshTriplett wrote:
               | Fair enough, if we could create generators and futures
               | that switch stacks that might work.
        
         | ReactiveJelly wrote:
         | > The big HTTP libraries let you opt out (of async)
         | 
         | At least for reqwest it just wraps the async code in something
         | like `block_on`:
         | https://github.com/seanmonstar/reqwest/blob/5397d2cf8eaecc9f...
         | 
         | And as the sibling comment says, you can do that in your own
         | code. It does still add Tokio to your binary size, and add some
         | compile time, and probably start a bunch of worker threads you
         | don't need, but it does work.
        
       | ThePhysicist wrote:
       | I recently got started with Rust and was surprised that different
       | async runtimes were not compatible with each other. In principle
       | Rust has a similar interface concept like Golang, so it should be
       | possible to specify desired behavior of a component and leave
       | implementation to the library, so that you can switch between
       | different ones without worrying about compatibility. Pretty much
       | a Rust noob still so maybe I'm missing something that makes this
       | difficult/impossible though.
       | 
       | I've been thinking about rewriting a network library using async,
       | but the whole async ecosystem seems a bit fragmented and
       | immature: mio would probably be everything I need but it doesn't
       | support channels (there's mio-extras which does but it's not
       | compatible with the latest mio version). Tokio would probably fit
       | the bill, though it seems to be too complex for what I actually
       | need (just a way to poll sockets and channels to see if there's
       | anything to read from them).
        
         | nicoburns wrote:
         | > Pretty much a Rust noob still so maybe I'm missing something
         | that makes this difficult/impossible though.
         | 
         | It's largely just because the various library authors have not
         | managed to agree on interface definitions. I think it'll get
         | sorted eventually, but unfortunately doesn't seem to be a big
         | priority for the runtime developers. It's also partially
         | blocked on async function being available in traits, which
         | isn't currently possible in Rust without workarounds (which
         | wouldn't be suitable for the standard library).
         | 
         | > Tokio would probably fit the bill, though it seems to be too
         | complex for what I actually need
         | 
         | Tokio is probably what you want. It might be complex under the
         | hood, but it ought to fairly straightforward to write
         | networking code using it (I believe polling a channel is
         | typically as simple as calling `.recv().await` in a loop within
         | an async function).
        
         | laerus wrote:
         | This is not unique to Rust. The Python async runtimes are not
         | compatible with each other as well, there is AnyIO which is a
         | wrapper that acts as an abstraction that makes things easier
         | but it's still not as implicit as with languages with builtin
         | runtimes.
         | 
         | There is also an ongoing effort to make Rust async runtimes
         | pluggable.
        
       | samsquire wrote:
       | The behind the details of Rust async is rather hard to follow
       | There's waiters and polling that only execute your function to
       | progress and there's pending and done. Any help to understand the
       | relationship between the waiters, pollers and executor and
       | runtime things would be appreciated.
       | 
       | I wrote a M:N thread scheduler in C, Java and Rust. The C version
       | also can schedule file reading to an IO thread but I'm nowhere
       | near finished.
       | 
       | https://github.com/samsquire/preemptible-thread
       | 
       | Another of my ideas is to rewrite synchronous code into parallel
       | LMAX disruptors. In other words a tree of RingBuffer each line of
       | synchronous code its own event loop. Rather than one event loop
       | multiplexing events from different systems you pipeline every
       | blocking call. I think it would be very fast.
       | 
       | Here's a write-up.
       | 
       | https://github.com/samsquire/ideas4#51-rewrite-synchronous-c...
        
       | armchairhacker wrote:
       | Basically, async Rust requires you to understand how "async"
       | works under the hood. Javascript async does stuff automatically,
       | there is an implicit runtime and you can just await or .then and
       | get a Promise and closure which encapsulates whatever data you
       | need and stores it on the heap.
       | 
       | Rust lets you optimize the runtime and do polling etc. how you
       | want. But you have to do everything explicitly. And you can store
       | futures on the stack and customize how their represented
       | (including the closures), but you have problems like async traits
       | because every future has a different type.
       | 
       | Lots of people say "async Rust is hard because async is hard".
       | Honestly this is false, async is easy if you Box dyn everything
       | and use the tokio runtime. Async Rust is hard - and Rust in
       | general is hard - because they don't compromise performance for
       | abstraction and encapsulation. You get exposed to the gritty
       | internals of how async works in exchange for being able to make
       | futures and a runtime which are optimal for your specific
       | program.
        
         | bkolobara wrote:
         | > Async Rust is hard - and Rust in general is hard - because
         | they don't compromise performance for abstraction and
         | encapsulation.
         | 
         | I love Rust, but this is not completely true. Async Rust is
         | hard because many nice features from Rust are not available in
         | async Rust. For example, you can't have async functions in
         | traits and always need to box return value, compromising
         | performance just because the language is not powerful enough
         | yet to support this.
         | 
         | This also leads to not having standard Read and Write traits
         | and a bunch of fragmentation between runtimes. It's not that
         | having it would compromise performance, it would even allow
         | better performance than the current workarounds, but async Rust
         | is still in development and needs time to catch up. In the
         | meantime you need to do a bunch of awkward tradeoffs between
         | ergonomics and performance when writing async Rust and of
         | course it's frustrating to developers that are used to Rust
         | being zero-cost with awesome ergonomics.
        
           | gloryjulio wrote:
           | This is very similar to c++. After transitioning to coroutine
           | from previously semicomplete implementations like promises,
           | our codebase become so much better. It just takes time to
           | reach a better feature
        
             | mrfox321 wrote:
             | what c++ coro library are you using, folly?
             | 
             | i use that and it makes async programming trivial, relative
             | to callback style programming.
        
         | moonchrome wrote:
         | JavaScript also has the benefit of a single threaded event loop
         | - that eliminates a whole class of complexity and dealing with
         | schedulers etc. - there is only one.
         | 
         | Async already gets messier in C# for example which also has a
         | GC.
        
           | armchairhacker wrote:
           | You can have a single threaded event loop in Rust. Just, like
           | everything else, it's an option and you have to specify it
           | explicitly.
        
             | dagmx wrote:
             | Yes but the point was that rust has to allow for multi
             | threaded event loops. JavaScript can design around knowing
             | that's not an option
        
             | moonchrome wrote:
             | Yes but the API has to be built for multithreaded
             | scheduling.
        
               | staticassertion wrote:
               | No, you get !Send futures.
        
               | Gwypaas wrote:
               | This is a nice read on why you likely should prefer a
               | single threaded runtime and enable parallelism through
               | other means. Or decide to pay the synchronization cost.
               | 
               | https://maciej.codes/2022-06-09-local-async.html
        
           | ashish_negi_ wrote:
           | if you write harder concurrent program in JS, single threaded
           | event loop does not help. It still has also same concurrency
           | problems. e.g. if there is shared global mutable state: a) in
           | workflow 1 you have, do_some_work_on_global_state; do some
           | IO; in IO callback, finish more do_some_work_on_global_state.
           | b) in workflow 2 .. same like above work on global_state.
           | 
           | Now, in IO callback, you don't know if workflow 2 ran and
           | have to handle all possible combinations of global_state
           | above.
           | 
           | Replace global_state with common_state and problem still
           | remains.
           | 
           | If you don't have common_state between multiple workflows,
           | then it is not a hard concurrency problem and should be easy
           | to do in all languages.
        
             | moonchrome wrote:
             | Sure it does, you don't have to manage on which thread you
             | handle continuations (which you must in multithreaded GUI
             | for example) - there's only one scheduler which simplifies
             | the async API a lot.
             | 
             | But even for concurrency - single threaded event
             | loop/cooperative multitasking eliminates a whole class of
             | partial state updates and synchronization primitive/locking
             | errors - it's not even close to preemptive multitasking
             | complexity.
        
         | mrits wrote:
         | I made a fairly complex app using tokio and async. I did not
         | know how async worked in rust at the time. I didn't even know
         | entirely why I needed tokio.
        
           | mattrighetti wrote:
           | This is a heated and frequent debate even in the rust
           | community itself: you almost always don't want/need async
        
         | staticassertion wrote:
         | Rust does not require you to know how async works under the
         | hood.
         | 
         | Javascript async doing things automatically has been infinitely
         | more confusing for me, frankly. Async rust isn't hard, rust
         | isn't hard - not for a lot of people at least.
         | 
         | I don't know what gritty details you're referring to, you need
         | to know the same rules you always know - move semantics, some
         | concept of lifetimes maybe. Move of the time it's "add a `move`
         | and clone before the async block".
        
           | Groxx wrote:
           | Fine-grained logic in async JavaScript can be a very special
           | kind of pain. It's a rather specialized event loop, but the
           | vast majority of articles treat it like "oh it's just a
           | normal in-order event loop like every other".
           | 
           | It ain't. Unless your logic has no order requirements between
           | async components, or explicitly accounts for things like
           | "microtasks", there's a chance it's wrong... and it depends
           | on your runtime: https://bytefish.medium.com/the-execution-
           | order-of-asynchron...
           | 
           | (it's generally better to not depend on execution order in
           | async systems anyway, but it's rather easy for it to sneak in
           | sometimes. if it does, it may work on your machine but not on
           | mine, or it might change based on what _kinds_ of tasks other
           | code spawns, if you press a button at a critical moment, etc)
        
           | thinkharderdev wrote:
           | In general I agree that the difficulty and need to know the
           | "gritty details" is overstated, but there is one aspect where
           | that is true and I have found it confusing at times. Since it
           | has to capture anything in scope of an await point, you will
           | sometimes get somewhat non-obvious compiler errors about how
           | "X is not Send" when it's not really obvious at all why it
           | would need to be. So something like
           | 
           | ``` let locked = std::sync::RwLock<Foo> = ...;
           | 
           | let lock = locked.write().unwrap();
           | 
           | bar.doSomethingAsync().await ```
           | 
           | will complain because `std::sync::RwLockWriteGuard` is not
           | send. Just looking at the code it is not really clear why it
           | should need to be. To understand why, you need to understand
           | how the compiler transforms this code into a state machine
           | and capture everything in scope of an await point in Struct
           | that must be send (since it can shift to new thread when
           | resuming). It makes sense when you understand what's
           | happening under the hood but can be a bit baffling when you
           | are starting out.
        
             | justinpombrio wrote:
             | That is tricky. It's also something you need to know when
             | working with closures in Rust, which are for the same
             | reason much harder to work with and understand than
             | closures in other languages. I wonder whether it would have
             | been better design for Rust closures to require an explicit
             | capture list, like in C++, just to be more explicit about
             | what is happening. (Not sure if/how that would translate to
             | `await`.)
        
         | amelius wrote:
         | As long as they've solved the "what color is your function"
         | ([1]) problem, I'm happy!
         | 
         | [1] https://journal.stuffwithstuff.com/2015/02/01/what-color-
         | is-...
        
       | saurik wrote:
       | So, as someone who has been working heavily with coroutines and
       | continuations for decades in a number of different languages
       | across the gamut of programming paradigms, I don't really
       | understand why these runtimes aren't "interoperable", and am
       | hoping I just have a different idea of what that word means than
       | the people who talk about them in the context of Rust.
       | 
       | Like, right now I maintain a large almost-entirely-asynchronous
       | C++ codebase using their new C++20 co_await monstrosity, and
       | while I find the abstraction ridiculously wide and a bit obtuse,
       | I have never had trouble "interoperating" different "runtimes"
       | and I am not even sure how one could screw it up in a way to
       | break that... unless maybe these "executors" are some attempt to
       | build some kind of pseudo-thread, but I guess I just feel like
       | that's so "amateur hour" that I would hope Rust didn't do that
       | (right?).
       | 
       | So, let's say you are executing inside of a coroutine (context is
       | unspecified as it doesn't matter). When this coroutine ends it
       | will transfer control to a continuation it was given. It now
       | wants to block on a socket, maybe managed by Runtime A (say,
       | Boost ASIO). That involves giving a continuation of this
       | coroutine past the point of the transfer of control to Runtime A
       | which will be executed by Runtime A.
       | 
       | Now, after Runtime A calls me--maybe on some background I/O
       | thread--I decide I would prefer y task to be executing in Runtime
       | B. I do this sometimes because I might have a bit of computation
       | to do but I don't want to block an I/O thread so I would prefer
       | to be executing inside of a thread pool designed for slow
       | background execution.
       | 
       | In this case, I simply await Runtime B (which in this case
       | happens to be my lightweight queue scheduler). I don't use any
       | special syntax for this because all of these runtimes fully
       | interoperate: I used await to wait for the socket operation and
       | now I use await to wait until I can be scheduled. The way these
       | control transfers work is also identical: I pass a continuation
       | of myself after the point of the await to the scheduler which
       | will call it when I can be scheduled.
       | 
       | Now remember, at the beginning of this I was noting that
       | something unspecified had called me. That is ostensibly a Runtime
       | C here (maybe I was waiting for a callback from libwebrtc--which
       | maintains its own runloop--because I asked it to update some ICE
       | parameter, which it does asynchronously). It doesn't matter what
       | it was, because now that "already happened": that event occurred
       | and the continuation I provided was already executed and has long
       | since completed _and returned_ as I went on immediately to pass a
       | continuation to someone else rather than blocking.
       | 
       | Is this somehow not how Rust works? Is await some kind of magic
       | "sticky" mechanism that requires the rest of this execution
       | happen in the context of the "same" runtime which is executing
       | the current task? I have seen people try to do that--I am looking
       | at you, Facebook Folly--but, in my experience, attempts to do
       | that are painfully slow as they require extra state and cause the
       | moral equivalent of a heavyweight context switch for every call
       | as you drag in a scheduler in places where you didn't need a
       | scheduler.
       | 
       | But, even when people do that, I have still never had an issue
       | making them interoperate with other runtimes, so that can't be
       | the issue at its core. I guess I should stare at the key place
       | where the wording in this article just feels weird?... to me, I/O
       | and computation are fairly disjoint, and so I can't imagine why
       | you would ever want to have your I/O scheduler do "double-duty"
       | to also handle "task queues". When I/O completes it completes:
       | that doesn't involve a "queue". If you want to be part of a
       | queue, you can await a queue slot. But it sounds like tokio is
       | doing both? Why?
        
         | dagmx wrote:
         | You can interchange async implementations in rust if you like,
         | much like you can in C++ or other languages.
         | 
         | What becomes hard though is grappling with what that means:
         | 
         | - the stdlib doesn't know about async, so there are a variety
         | of async stdlibs that may or may not be tightly coupled to an
         | implementation.
         | 
         | - different runtimes may choose different threading models.
         | Some may be single threaded-ish, some may be across threads.
         | You could treat it all like it's across threads, but this does
         | mean that there's another detail you need to consider when
         | you're setting up your data.
         | 
         | - Io scheduling mixed with task scheduling is a choice of how
         | an async stdlib is configured. There's advantages to having
         | them coupled in that the runtime can sort checks on returns on
         | the Io call as it cycles through the tasks, or put them all on
         | a single thread queue etc... There's lots of patterns here that
         | may have their own individual tradeoffs
        
         | haradion wrote:
         | You might think of Rust's async paradigm as "half a
         | continuation, turned upside down". With traditional coroutines,
         | after an async operation completes, the language's runtime
         | calls back into your code, and you actively call the next
         | thing, "pushing" control flow down the pipe. Most languages
         | with continuations manage this by "pausing" your function and
         | keeping its stack frame around, which, in the general case,
         | means your function's stack frame has to be heap-allocated,
         | which is basically the language itself giving you a "pseudo-
         | thread". You eventually get control back with the same stack
         | frame, and as far as the language is concerned, how you get
         | back there is none of your concern; that's its job.
         | 
         | In Rust's polling-based model, there's no "magic" saving of
         | stack frames. You get some space to store state, but the
         | runtime has to manage that memory itself. You can use the
         | language to express "this is the next thing to call", but when
         | you spawn an async I/O task and yield to it, you've already
         | returned from your own function to the runtime, and it's the
         | runtime's job to call your function again with the state it had
         | stashed away. You then jump over the steps in your function
         | that have already been handled and call into the next thing. It
         | gets a bit more involved due to various bits of syntactic
         | sugar, but that's the basic model. It's operating at a lower
         | level of abstraction than many languages' coroutines or
         | call/cc, which gives you the flexibility to customize the
         | behavior to meet specific needs.
         | 
         | A runtime for generic desktop/server apps may maintain a thread
         | pool and call back into your code on one of those threads. In
         | WebAssembly, execution is single-threaded, but JavaScript
         | promises may call into your runtime, and you have to dispatch
         | that to the right Rust future. On embedded platforms, the data
         | structures that the desktop/server runtime uses may simply not
         | be suitable (e.g. because you have no general-purpose heap
         | allocator), so you need to use a different approach with more
         | constraints.
         | 
         | Interoperability between these runtime is possible. The key is
         | that you need a task that's running on one runtime to be able
         | to spawn a task on the other, with part of that task's job
         | being to notify the first runtime that it's time to poll the
         | "parent" task again. The mechanics vary depending on how each
         | runtime handles task spawning.
         | 
         | As I understand it (from having skimmed some articles a while
         | back), C++'s co_await isn't really all that different. Since we
         | don't have the executors proposal as part of the standard yet,
         | it's still a "bring-your-own runtime" sort of approach, with
         | some kind of glue required at the boundaries between runtimes.
         | Depending on which "flavor" of C++ coroutines you're using
         | (e.g. push-based vs. pull-based), that interop might be easier
         | than Rust's at the cost of other tradeoffs (e.g. more heap
         | allocations).
        
         | jalk wrote:
         | I think the issue in question is more mundane - if someone
         | publishes an async database client library, its currently
         | hardwired to a specific async runtime, so you cannot not easily
         | use it if you are not already using that runtime. The common
         | async abstraction being worked on, sets out to solve that.
        
           | lvass wrote:
           | >hardwired to a specific async runtime
           | 
           | With version constraints, right? IIRC you can end up with
           | multiple versions of multiple async runtimes in a project. I
           | think it'd be better to only have a single one hardwired to
           | the compiler like python's asyncio, even if it likewise
           | sucked.
        
             | nicoburns wrote:
             | In practice both major runtimes have long-term stability
             | guarantees (e.g. tokio has committed to maintained 1.0 for
             | at least 5 years), so if you use libraries compatible with
             | Tokio 1.0 then you're unlikely to have issues with this for
             | some time.
        
             | craftkiller wrote:
             | Rust libraries can have implementations for each async
             | runtime and then you can pick between them using features.
             | For example, when using sqlx with tokio I would have this
             | in my Cargo.toml:                   [dependencies]
             | sqlx = { version = "0.5", features = [ "runtime-tokio-
             | rustls", "sqlite", "migrate" ] }
             | 
             | But I also could use async-std with:
             | [dependencies]         sqlx = { version = "0.5", features =
             | [ "runtime-async-std-rustls", "sqlite", "migrate" ] }
             | 
             | So you should be able to get all your deps on a single
             | runtime.
        
             | WaxProlix wrote:
             | I haven't used much Python recently, but iirc you can just
             | import your own runtimes, too. Twisted, gevent, that sort
             | of thing. Having some sort of sane defaults bundled gives
             | you a really nice baseline for interop, but doesn't
             | preclude you from picking things that fit your use case
             | better.
             | 
             | Definitely feels like one place where Rust kinda dropped
             | the ball, at least from a user perspective in $CURRENTYEAR.
        
               | lvass wrote:
               | True, what I meant is that asyncio is part of the
               | interpreter so you are very unlikely to have trouble with
               | incompatible versions of asyncio. Twisted and gevent
               | don't use async/await, but there's Trio which does and is
               | saner than asyncio, but thankfully library authors aren't
               | forcing it's usage. It's also possible to write libraries
               | that use async but you bring your own runtime (trio or
               | asyncio) with AnyIO.
        
           | saurik wrote:
           | I mean, I would hope "easily" would happen because I can
           | always just use two async runtimes... if they were
           | "interoperable". I can quite easily have a number of separate
           | I/O abstractions and schedulers all happening at the same
           | time in C++, for example, and I never think much about it: I
           | just co_await and it, well, waits.
        
         | nextaccountic wrote:
         | They are interoperable in the most basic mechanism of futures:
         | every executor can spawn tasks composed of any futures (just
         | like co_await in C++ is interoperable)
         | 
         | But they aren't interoperable in practice because they offer
         | different APIs
         | 
         | In some cases this is fixable (for example, the rust ecosystem
         | needs some to standardize some async abstractions because
         | currently every executor defines their own trait for async
         | reading for example), in other cases it represents a genuine
         | limitation of a given executor (for example, some embedded
         | executors can only spawn a single task, and you achieve
         | concurrency by using future combinators)
        
           | saurik wrote:
           | OK, so the version of "interoperable" you seem to be using
           | sounds like like "swappable", which isn't really a property I
           | have ever cared much about. Like, if I have code that is
           | using ASIO's task abstraction and other code using cppcoro's
           | and other code using my own scheduler and still other code
           | wired up over some callback setup, I would have just used
           | "interoperable" to mean I can await whatever I want whenever
           | I want without complex glue code, as--at the end of the day--
           | I am merely passing a continuation for my function to someone
           | who will call it later. I mean, _of course_ the APIs aren 't
           | the same: in one case I am awaiting sockets and in other case
           | I am awaiting queue slots and in another case I am awaiting
           | random asynchronous events but I am able to do all of it from
           | a single asynchronous function as they are all
           | "interoperable". It just sounds from these articles that Rust
           | can't even do that.
        
       | swayvil wrote:
       | Ever notice that "rust" is a contraction of "red dust"?
       | 
       | (Also, words are usually first coined as onomatopoeias. For
       | example "dust" sounds like a swisshing pile of dust. But "red".
       | That probably has more stuff going on there. Added layers)
        
       | aaaaaaaaaaab wrote:
       | So, in other languages, async introduces two function "colors":
       | sync and async. But in Rust you also have unique "colors" for
       | each async runtime?
       | 
       | I must be misunderstanding something, because this sounds like
       | pretty hare-brained...
        
         | ibraheemdev wrote:
         | This is a consequence of runtimes relying on global variables
         | that their core future types are dependent on. Creating
         | abstractions to solve this problem is one of the main goals of
         | the the async working group [0].
         | 
         | [0]: https://github.com/rust-lang/wg-async
        
           | stormbrew wrote:
           | It would be really interesting to see an executor that didn't
           | rely on global state, but instead had you manage an executor
           | object for these things, and see if it's actually really all
           | that bad. Everyone just kind of jumped straight on doing
           | executors with global state and that design space is just
           | completely unexplored.
           | 
           | It would help a lot with being able to discover and define
           | what standard traits might be needed to make leaf async
           | libraries more portable.
        
         | yazaddaruvala wrote:
         | This is only temporary.
         | 
         | It's because the standard library hasn't exposed interfaces for
         | everything in async land just yet.
         | 
         | It's not a trivial problem, but it's mostly a matter of the
         | std-lib's developers being overly defensive of std. Which is a
         | hindrance for many things in the medium term, but in the long
         | term likely a good choice.
         | 
         | Although I wish they'd find a better balance between the
         | chicken and egg problem for "we need production usage of the
         | interfaces to validate them as standard" vs "we need standard
         | interfaces so it's ergonomic to use them in production".
        
           | lvass wrote:
           | Async/await should be locked behind a compiler flag and not
           | used by libraries that aren't experimental. Instead it
           | already contaminated the entire ecosystem with to-be-
           | deprecated dependencies that are hard to remove.
        
             | aaaaaaaaaaab wrote:
             | Yep. I can't help but see this as a python2.7 vs python3
             | story in the long run...
        
             | stormbrew wrote:
             | Rust wouldn't be anywhere near where it is now if it had
             | gated and blocked async like this. Maybe it would be
             | 'better' in some abstract sense, but it'd be a better
             | language almost no one would be using, like basically every
             | other systems language that's attempted to do what rust is
             | doing other than C++.
             | 
             | And if it were gated like this, we wouldn't even _know_
             | about these kinds of problems, because no one would be
             | using it and we wouldn 't see them in practice.
             | 
             | If anything, rust does _too much_ gating of features for
             | too long. So many things have been sitting behind  "needs
             | stabilization" for years, living in a catch-22 of "we don't
             | know if this is a good idea because no one uses it" vs. "no
             | one uses it because using nightly is scary". I'm quite glad
             | this one managed to escape that trap.
        
         | ReflectedImage wrote:
         | More colors the better! Right?
        
         | ben-schaaf wrote:
         | You get the same thing in every language where you can choose a
         | runtime. Whether the colors are enforced by the compiler is
         | another matter, but since you can only have one runtime per
         | thread and each runtime has a different API you inherently get
         | runtime colors.
        
           | aaaaaaaaaaab wrote:
           | >you can only have one runtime per thread
           | 
           | Why?
           | 
           | Why can't a single thread run multiple runtimes' event loops?
           | It's pretty limited what an event loop can do: it can wait on
           | a set of FDs, or sleep for some time (or until woken up). So
           | if the various runtimes' event loops could implement a common
           | interface, then I don't see why they couldn't share a single
           | thread...
        
             | ben-schaaf wrote:
             | > It's pretty limited what an event loop can do: it can
             | wait on a set of FDs, or sleep for some time (or until
             | woken up).
             | 
             | But that's what the runtime is. There's multiple strategies
             | for implementing an event loop (as well as multiple async
             | platform APIs to use), which all directly affect the lower-
             | level abstractions. By replacing the event loop and lower-
             | level abstractions with one common implementation all
             | you've done is add another competing runtime. You can't
             | have more than one event loop and thus you can't have more
             | than one runtime.
        
             | mcronce wrote:
             | If you've got a thread running one event loop, how could
             | you signal to it to temporarily break out of that loop and
             | run a different event loop for a while?
        
               | aaaaaaaaaaab wrote:
               | The while(1) part of the event loop would be outside,
               | provided by the stdlib. Each runtime would provide a set
               | of FDs that they want to monitor, and a function to
               | execute a single iteration of their event loop. The
               | while(1) loop would then be calling each runtime in a
               | round-robin fashion, then go sleep while waiting on the
               | union of all the runtimes' FD sets. It would be sort of
               | an "event loop of event loops".
        
               | stormbrew wrote:
               | What you've just described is ... an executor runtime.
               | Even if you boil it down to doing "only" that, you still
               | can only have one of that per thread, and there isn't
               | just one way to implement that.
        
         | pornel wrote:
         | The original "colors" article talked about two things:
         | 
         | 1. Inability for sync functions to use async functions, which
         | can have big consequences for API design and ability to
         | refactor applications.
         | 
         | 2. Author's opinion on how async syntax should (not) look like.
         | 
         | Rust can mitigate the first problem. It can have
         | `block_on(async)` and `spawn_sync(fn)` which allows bridging
         | between "colors" of functions, so functions aren't forever
         | stuck with their "color". This is something that JS can't do
         | without massive hacks, and is the objectively important aspect
         | of "colors".
         | 
         | The other thing was about difference in calling syntax. That is
         | just a design choice and a subjective preference. Rust prefers
         | locally explicit syntax. It cares about low-level details and
         | intentionally avoids having implicit magic, especially for
         | major behaviors affecting control flow, safety of stack
         | pointers, and risk of deadlocks.
         | 
         | Regarding runtimes: in practice it's easy to just stick to
         | tokio. It's 8x more popular than the second contender, and
         | there aren't any important libraries that don't work with
         | tokio. Rust can have multiple runtimes in the same program. You
         | can have futures running on different runtimes await each
         | other, it's just wasteful (you get multiple event loops, thread
         | pools, etc.), which means it's best to pick one and stick with
         | it.
        
       ___________________________________________________________________
       (page generated 2022-07-16 23:01 UTC)