It was really hard to build asynchronous code until now. You had to clone objects used within futures. You had to chain asynchronous calls together. You had to bend over backwards to support conditional returns. Error messages weren't very explanatory. You had limited access to documentation and tutorials to figure everything out. It was a process of walking over hot coals before becoming productive with asynchronous Rust.
Now, the story is different. Further a few heroes of the community are actively writing more educational materials to make it even easier for newcomers to become productive with async programming much faster than it took others.
Refactoring legacy asynchronous code to async-await syntax offers improved readability, maintainability, functionality, and performance. It's totally worth the effort. Do your due diligence in advance, though, and ensure that your work is eligible for refactoring. Niko wasn't kidding about this being a minimum viable product.
I was trying to refactor one of my Rust project the other day and almost immediately got hit by the "No async fn in traits" and the "No lifetime on Associated Type" truck. Then few days later, comes this article: https://news.ycombinator.com/item?id=21367691.
If GAT can resolve those two problems, then I guess I'll just add that to my wish list. Hope the Rust team keep up the awesome good work :)
Async/await lets you write non-blocking, single-threaded but highly interweaved firmware/apps in allocation-free, single-threaded environments (bare-metal programming without an OS). The abstractions around stack snapshots allow seamless coroutines and I believe will make rust pretty much the easiest low-level platform to develop for.
Céu is the more recent one of the two and is a research language that was designed with embedded systems in mind, with the PhD theses to show for it [2][3].
I wish other languages would adopt ideas from Céu. I have a feeling that if there was a language that supports both kinds of concurrency and allows for the GALS approach (globally asynchronous (meaning threads in this context), locally synchronous) you would have something really powerful on your hands.
EDIT: Er... sorry, this may have been a bit of an inappropriate comment, shifting the focus away from the Rust celebration. I'm really happy for Rust for finally landing this! (but could you pretty please start experimenting with synchronous concurrency too? ;) )
[1] https://en.wikipedia.org/wiki/Esterel
[2] http://ceu-lang.org/chico/ceu_phd.pdf
[3] http://sunsite.informatik.rwth-aachen.de/Publications/AIB/20...
I think if it's interesting and spurs useful conversation, it's appropriate, tangent or not. I for one an thankful for your suggested links, they look interesting.
Ceu looks very neat, I suspect (having not read much about it yet) that async codebases could take a lot of inspiration from it already.
That sounds very promising, I should give it a closer look (I don't program in Rust myself so I only read blogs out of intellectual curiosity)
If you don't want to deal with async functions, then you can use threads! That's what they're there for. On Linux they're quite fast. Async is for when you need more performance than what 1:1 or M:N threading can provide.
> But if you have threads (green- or OS-level), you don’t need to do that. You can just suspend the entire thread and hop straight back to the OS or event loop without having to return from all of those functions.
Correct me if I'm wrong, but wasn't the lack of threads one of the biggest reasons why NodeJS originally outperformed most of its competitors?
Spinning up threads for each concurrent request was expensive, and (nonblocking) async code was by comparison ridiculously cheap, so the lack of overhead meant Node could just make everything async, instead of trying to decide up-front which tasks deserved which resources.
Granted, it's been over a decade since Node came out. Maybe thread overhead has gotten a lot better? But barring faulty memory, I definitely remember a number of people explaining to me back then that being single-threaded was the point.
I'll open a thread on users.rust-lang.com to discuss the similarities/differences with Céu, but for now, here's the main similarity I see:
A Céu "trail" sounds a lot like an `async fn` in Rust. Within these functions, `.await` represents an explicit sync-point, i.e., the function cedes the runtime thread to the concurrency-runtime so that another `async fn` may be scheduled.
(The concurrency-runtime is a combination of two objects, an `Executor` and a `Reactor`, that must meet certain requirements but are not provided by the language or standard library.)
Having said that, the combination of par/or and par/and constructs for parallel trails of execution, and code/await for reactive objects is like nothing I have ever seen in other languages (it's a little bit actor-modelish, but not quite). I haven't looked closely at Rust though.
Céu also claims to have deterministic concurrency. What it means by that is that when multiple trails await the same external event, they are awakened in lexical order. So for a given sequence of external events in a given order, execution behavior should be deterministic. This kind of determinism seems to be true for all synchronous reactive languages[0]. Is the (single-threaded) concurrency model in Rust comparable here?
The thesis is a bit out of date (only slightly though), the manual for v0.30 or the online playground might be a better start[1][2].
[0] which is like... three languages in total, hahaha. I don't even remember the third one beyond Esterel and Céu. The determinism is quite important though, because Esterel was designed with safety-critical systems in mind.
This can let you avoid a lot of the pomp and circumstance of writing state machines by hand.
Your explanations were surprisingly simple
I'm still wondering if lots of the problems can be solved with an "allocate only on startup" instead of a "never allocate" strategy, or whether full dynamical allocation is required. Probably needs some real world apps to find out.
Errr, no. You either poll, or set up an interrupt to catch an event. The point of an interrupt is to allow other code to continue to run while waiting for a peripheral.
It's for cases where alternatives don't exist or they are too expensive.
Language level green threads are safer abstraction over asynchronous I/O operations.
This stuff is easily available for C. It's just a function call instead of syntactic sugar. On Windows, use Fibers. On Linux, there is a (deprecated) API as well (forget the name). Or use a portable wrapper library.
Or just write a little assembly. Can't be hard.
Async I/O gives awesome performance, but further abstractions would make it easier and less risky to use. Designing everything around the fact that a program uses async I/O, including things that have nothing to do with I/O, is crazy.
Programming languages have the power to implement concurrency patterns that offer the same kind of performances, without the hassle.
I know Rust is all about zero-cost abstractions, "but at what cost" beyond just runtime cost? I appreciate their principled approach to mechanical sympathy and interop with the C abstract machine, but I'm just not enthused about this particular tradeoff.
An alternative design would have kept await, but supported async not as a function-modifier, but as an expression modifier. Unfortunately, as the async compiler transform is a static property of a function, this would break separate compilation. That said, I have to wonder if the continuously maturing specialization machinery in Rustc could have been used to address this. IIUC, they already have generic code that is compiled as an AST and specialized to machine code upon use, then memoized for future use. They could specialize functions by their async usage and whether or not any higher-order arguments are themselves async. It might balloon worst-case compilation time by ~2X for async/non-async uses (more for HOF), but that's probably better than ballooning user-written code by 2X.
For instance, Go does not give you the option to handle this yourself: you cannot write a library for implementing goroutines or a scheduler, since it's embed in every program. That's why it's called runtime. In Rust, every bit of async implementation (futures, schedulers, etc) is a library, with some language support for easing type inference and declarations. This should already tell you why they took this approach.
Regarding async/await and function colors (from the article you posted), I would much rather prefer Rust to use an effect system for implementing this. However, since effects are still much into research and there is no major language which is pushing on this direction (maybe OCaml in a few years?) it seems like a long shot for now.
Another difference with Rust is that async functions are stackless. Go has to contend with having a non-conventional stack and interoperability with C code (and sometimes syscalls, vDSO, etc.) that expects a relatively large stack requires pivoting.
I do wish Rust could solve this problem, but the two approaches are very different indeed. I think it’s a fact of life that accidental blocking will exist in Rust, approaches that prevent it are complicated and indeed have runtime costs.
I did not suggest that it do so. In fact, I suggested an approach that wouldn't fit for Go!
In Go, you've got pretty strict modular compilation. Because of this, specialization -- which is pervasive in Rust -- is virtually absent in Go. Rust's rich generics system demands robust specialization machinery. This machinery, which handles instantiation of explicit generics, can be repurposed to handle automatic instantiation of implicit generics. In this case, I'm suggesting that a function could be compiled as either synchronous or asynchronous as determined by the use-site.
> In Rust, every bit of async implementation (futures, schedulers, etc) is a library
Nothing about my suggestion precludes this.
So you have a synchronous http server and you decide you want to make it async? Ok, no problem, switch to an async-enabled request handler in main, and boom everything that you wrote is recompiled into a state machine a la async, and at the very bottom where it actually calls the library to write out to the socket it, the library knows what the context is and can choose the async-enabled implementation.
I'm glossing over some important details and restrictions that might make this more complicated in practice, but I think it should at least be possible for functions to opt-in to 'async-specializability' to avoid having to rewrite the world for async.
How is that different than an `async` block?
Can you give one that reaches this goal? Go is often cited on that regard but it doesn't really fit your description since it trades performance for convinience (interactions with native libraries are really slow because of that) and still doesn't solve all problems since hot loops can block a whole OS thread, slowing down unrelated goroutines. (There's some work in progress to make the scheduler able to preempt tigh loops, though).
https://cr.openjdk.java.net/~rpressler/loom/Loom-Proposal.ht...
However, Project Loom doesn't fit into the "zero cost abstraction" paradigm of rust. Project Loom requires quite a lot of support form the JVM runtime:
> The main technical mission in implementing continuations — and indeed, of this entire project — is adding to HotSpot the ability to capture, store and resume callstacks not as part of kernel threads. JNI stack frames will likely not be supported.
It also still require manual suspension so JIT compiled tight loops will likely still cause a problem.
Of course Python generally fails to offer "the same kind of performance" for anything limited by CPU or memory, so it technically doesn't fit the description either.
Asking as a beginner, what does the above mean?
Not sure what does hot loop means, and why does it block Os thread
OS threads can be expensive, so libraries try to only create a few OS threads.
A “hot loop” (usually, I think, called a “tight loop”) is a loop that does a lot of work for a relatively large amount of time all at once. Things like “looping through a list of every building an Manhattan and getting the average price over two decades.”
With some code like networking, you end up having to wait on other parts of the computer besides the CPU often, for things like uploads and downloads.
“Asynchronous programming” tried to make it easy to keep the CPU busy doing helpful things, even while some of it’s jobs are stuck waiting on uploads/downloads/whatever. This keeps the program efficient, because it can do a little bit of many tasks at once instead of having to complete each task entirely before moving on to the next.
The problem comes when you have a tight loop in a thread that is mostly expecting to be doing asynchronous work around I/O or networking. It is basically trying to juggle with one hand. The program can’t multitask as well, and you end up having to wait longer before it can start each piece of work you want.
If you're doing a hot loop, which may be synchronous, you will block the event loop attached to that OS thread, because a hot loop is presumably not yielding control until it is done.
Can you explain a bit? What is the connection between concurrency implementation (which I am assuming you are talking about multiplexing multiple coroutines over the same OS thread) and say slowness in cgo? Having to save the stack? I don't get it.
First-class continuations. They are an efficient building block for everything from calling asynchronous I/O routines without turning your code into callback spaghetti, to implementing coroutines and green threads. Goroutines are a special and poorly implemented case of continuations. Gambit-C has had preemptive green threads with priorities and mailboxes at less than 1KiB per thread memory overhead for over a decade now, all built on top of first-class continuations.
If you take on a few limitations you can get there, but then those are exactly the things people complain about.
If you use a work stealing executor tasks will get executed on another thread. Therefore the impact of accidentally blocking is lowered. Tokio implements such an executor
I think the question you're asking is, "are deadlocks possible?" and the answer to that seems like it would be yes. I would hope that Rust's memory model makes accidental memory sharing dependencies & deadlocks harder to cause, but you can always create 8 threads waiting on a mutex and a 9th that will release it, and have the 8 spinlock on the waiting part.
The "async-std" library, one of a few attempts to write async-aware primitive functions around blocking calls to the filesystem, mutexes, etc, implements async wrappers around mutex and others that should ensure that yield points are correctly inserted and the task goes back into the queues if it's blocked.
That seems to me the right solution - make sure all your blocking calls with a potential for deadlocking check "am I blocked?" first and if so, put themselves back onto the task queue instead of spinning.
Microsoft kind of tried to do this with the new APIs for UWP: pretty much everything is async, the blocking versions of APIs were all eliminated, so there was no way for the async-ness to "infect" otherwise synchronous code. It was actually a pretty nice way to program; it's a shame it never took off.
They also lowered their portion of the revenue share considerably for Store apps, afaik.
So until there's some standardized form of seamless coroutines on OS level, that is sufficiently portable to be in wide use, we won't see widespread adoption of them outside of autarkic languages and ecosystems like Erlang or Go.
This is the value people get out of async only environments like node. Yes, you still have to worry about costly for loops but I don't have to worry about which database driver I'm using because they are all async. In a mixed paradigm language like rust I would really appreciate the compiler telling me I grabbed the wrong database driver.
I think the best you could do would be heuristics - having inferred or user-supplied bounds on the complexity of functions, having rough ideas on how disk or network latency will affect the performance of functions, and bubbling that information up the call tree. It wouldn't be perfect, but it could be useful.
You can even express algorithmic complexity in a language.
But it's a bit more complex than that, really. You will have a harder time saying "this loop will block the event system for longer than I'd like".
1. A performant "i/o" layer in the standard library that allows a large number of concurrent activity (forget thread vs coroutine differences).
2. Ability of programmer to express concurrency. Ideally, this has nothing to do with "I/O". If I am doing two calculations and both can run simultaneously, I should be able to represent that. Similarly for wait/join.
Explicitly marking a thread as async-only will just force everyone else (who need sync and cannot track/return a promise/callback to their caller) write a wrapper around it for no reason.
Besides, you don't have to put async/await everywhere: if your code is not performing IO, it completely ignore this concern.
The problem is that most of your code is mixing I/O and non I/O code, and people just don't think about it. E.G: a django website is not just a web server, but has also plenty of calls to the session store, the cache backend, the ORM, etc.
Now you could argue that the compiler/interpreter is supposed to hide the sync/async choice to the code user. Unfortunately, this hides where the concurrency happens, and things have dependencies on each others. Some are exclusive, some must follow each others, some can be parallel but must all finish together at some points, some access concurrent resources...
You must have control over all that, and for that to happen, you can either:
- have guard code around each place you expect concurrency. This is what we do with threads, and it sucks. Locking is hard, and you always miss some race condition because it can switch anywhere. - have implicit but official switch points and silos you must know by heart. This is what gevent does. It's great for small systems, not so much at scale. - have explicit switch points and silos: async/await, promises, go-routine. This is tedious to write, but the dangerous spots are very clear and it forces you to think about concurrency upfront.
The last one is the least worse system we managed to write.
Welcome to the 1980s world of cooperative multitasking, but now with "multi-colored functions."
I don't know, maybe there are valid applications for await (such as much frequented web servers, where you might want to have 10s of thousands of connections, that would be too expensive to model as regular threads, but still you just want to get some cheap persistence of state and it's not a big problem that the state is lost on server reboot). I can't say, I'm not in web dev.
But I bet it's much more common that await is simply a little overhyped and often one of the other options (real threads or explicit state data structures) is actually a better choice.
Well... I can't help but whenever I see the await stuff it reminds me of times where I had to do cooperative multitasking and was longing for OS and/or CPU support for something which is non-invasive to my algorithms. But then I'm not sure whether I'm the grumpy old man or it is just history repeating.
The issue it solves is programmer having trouble executing parallel code in their head, and when relationship became intricate (a computation graph) they just breakdown and write buggy software.
A scheduler is targeted at use cases. A preemptive scheduler optimize for latency and fairness and would apply for real-time (say live audio/video work or games) but for most other use cases you want to optimize for throughput.
With real thread you risk oversubscription or you risk not getting enough work hence the task abstractions and a runtime that multiplex and schedule all those tasks on OS threads. Explicit state data structure is the bane of multithreading, you want to avoid state, it creates contention point, requires synchronization primitives, is hard to test. The beauty of futures is that you create an implicit state machine without a state.
The only question is, can you express some of the state-relations as serial code? If "relationships became intricate (a computation graph)", chances are, you shouldn't use serial code anymore, because that splits dependencies in a two-class society: those that are expressed using code and those that are expressed using dead data. It is usually preferable to specify everything as dead data if the relationships get complex, and to then code sort of a virtual machine that "executes" the data.
So it's in fact the simple cases that lend themselves to being expressed as serial code. I won't argue with that there are nice looking example usages. Problem is, as always, systems that help making the simple things easy often make the hard things impossible.
Maybe I just can't imagine it. Whats a good language that shows off the style you're suggesting?
What you describe sounds like native UI work since forever before javascript. "Don't block the main thread" and all that.
Javascript is diferent in that it's a single-thread with an event loop. Synchronous functions execute until they end. Asynchronous functions are handled by the event loop which "loops" between the pool and runs each one for some time, then switches to other, concurrenly (think round robin). What happens when the runtime is running an asynchronous function and inside it reaches a synchronous one? it stops round-robin and executes this function until it ends.
What OP wants is a language like javascript but without having to write code distinguishing synchronous and asynchronous functions and instead having some other tool to tell the runtime when a function is synchronous or asynchronous without having to write it again.
Still, in practice it's just not that hard, use async methods if you're writing async code.
No, it doesn't really. 'Async' is a strictly Python problem, due to the insanity of the GIL. Predictably, the Python solution to it is also insane.
Why you have to turn a sane language like Rust into an insane one by cargo-culting a solution to a non-problem is a mystery to me.
Oh well, good thing at least C++ hasn't dropped the ball.
> "While it is theoretically not an insurmountable challenge , it might be a major re-engineering effort to the front-end structure and experts on two compiler front ends (Clang, EDG) have indicated that this is not a practical approach."
So the short answer seems to be that due to technical debt on the part of existing C++ compilers and their "straight" pipeline, the front-end cannot anticipate the size necessary for some book-keeping information traditionally handled by the code-generator. I'll take Richard Smith's word on Clang.
- Asynchronous programming is completely orthogonal to Python
- If you don't need it, don't use it. Rust without asynchronous functions still looks and works like before
- By the way: welcome to C++20's coroutines
Theoretically, yes. In practice, people are just copying Python mistakes word-for-word, in an attempt to score the "coolness" factor that async garnered in the Python community. (Never mind that the "coolness" came from solving a problem that doesn't even exist in other languages...)
If you want to see what asynchronous programming would look like if it weren't copied from Python, look at the recent C++ proposal.
> - If you don't need it, don't use it. Rust without asynchronous functions still looks and works like before
The problem is that every Python library now comes in two versions, regular and 'async'. Even if you don't need or care about anything 'async'.
I guess splintering your code base into two incompatible flavors is just the pythonic way of doing things? Now that the 2 vs 3 insanity is finally dying down, they needed to start a fresh one?
Why is Rust trying hard to repeat these mistakes?
> - By the way: welcome to C++20's coroutines
That I already answered above.
The difference between synchronous code and async code implemented as libraries is that async code involves jumping in and out of functions a lot, while employing runtime library code in between. A piece of code that is conceptually straightforward, may, in the async case, involve multiple returns and restores. In the sync case it doesn't need to do that, since it just blocks the thread and does the processing in other threads and in kernel land.
Rust's async/await support makes it possible to write code that is structurally "straightforward" in a similar way than synchronous code would be. That allows the borrow checker to reason about it in a similar way it would reason about sync code.
BTW this is a big pain point for me (unrelated to async). Code like this:
let ref = &mut self.field;
self.helper_mutating_another_field();
do_something(ref);
gets rejected because self.helper_mutating_another_field() will mutably borrow the whole struct. The workaround is either to inline the helper or factor out a smaller substruct so that helper can borrow that which doesn't always look good.Of course it is preferable that all information needed for the caller to check if the call is correct is contained in the function signature but it truly is frustrating to see the function body right there, know that it doesn't violate borrowing rules and still get the code calling it rejected.
To further frame that question: I had assumed Rust was implementing Async within the capabilities of normal Rust. Such that, if lifetimes were being managed across function bounds, I assumed the lifetime would have "simply" been bound to the scope of the executor, polling the future you're actively waiting on.
However quickly I can see confusions in that description. Normal lifetimes within a function would need to bubble up and be managed by a parent, since that function's lifetime is, as you put it, being jumped in and out of frequently.
So I imagine that is partly where new features come into play? Giving Rust the ability to take a normal lifetime and extend or manage it in new ways?
I'd love for Rust to eventually get a Move trait (something like C=+ move constructors) to resolve this. Besides some complexity in designing it, there is resistance from some corners about having anything execute on a move.
This is the challenge and async await lets you make this kind of self referential types without unsafe code.
If you want to defer execution of a promise until you await it, you can always do that, but this paradigm forces you to do that. The problem is then, how do I do parallel execution of asynchronous tasks?
In JavaScript I could do
const results = await Promise.all([
asyncTaskA(),
asyncTaskB(),
asyncTaskC()
]);
and those will execute simultaneously and await all results.And that's me deferring execution to the point that I'd like to await it, but in JavaScript you could additionally do
const results = await Promise.all([
alreadyExecutingPromiseA,
alreadyExecutingPromiseB,
alreadyExecutingPromiseC
]);
Where I pass in the actual promises which have returned from having called the functions at some point previously.So how is parallel execution handled in Rust?
Begin execution where?
If every future started executing immediately on a global event loop, that event loop would need to allocate space for every future on the heap. A heap allocation for every future is exactly the sort of overhead that Rust is trying so carefully to avoid. With Rust futures, you can have a large call tree of async functions calling other async functions. Each one of those returns a future, and those futures get cobbled together by the compiler into a single giant future of a statically known size. Once that state machine object is assembled, you can make a single heap allocation to put it on your event loop. Or if you're going to block the current thread waiting on it, depending on what runtime library you're using, you might even get away with zero heap allocations.
This sort of thing is also why Rust's futures are poll-based, rather than using callbacks. Callbacks would force everything to be heap allocated, and would work poorly with lifetimes and ownership in general.
These issues are not an issue in JavaScript and other languages since objects are individually allocated on the heap there anyway
futures::join!(asyncTaskA(), asyncTaskB()).await
See the join macro of futures[0]. The way it works is, it will create a future that, when polled, will call the underlying poll function of all three futures, saving the eventual result into a tuple.This will allow making progress on all three futures at the same time.
Without a yield instruction its strange to ask "how do I start all these futures before I await" and join does make sense because it does both of those things. But other languages can start futures, yield, reenter, start more futures, and wait for them all while making progress in the mean time.
I'm curious what the plan in there.
A future in rust is really just a trait that implements a `poll` function, whose return type is either "Pending" or "Ready<T>". When you create a future, you're just instantiating a type implementing that function.
For a future to make progress, you need to call its poll function. A totally valid (if very inefficient) strategy is to repeatedly call the Future's poll function in a loop until it returns Ready.
All the `await` keyword does is propagate the Pending case up. It can be expanded trivially to the following:
match future.poll() {
Pending => return Pending,
Ready(val) => val
}
Now, when we create a top-level Future, it won't start executing. We need to spawn it on an Executor. The Executor is generally provided by a library (tokio, async-std, others...) that gives you an event loop and various functions to perform Async IO. Those functions will generally be your leaf-level/bottommost futures, which are implemented by having the poll function register the interest in a given resource (file descriptor or what not) so that the currently executing top-level Future will be waken up when that resource has progressed by the Executor.So if you want to start a future, you will either have to spawn it as a top-level future (in which case you cannot get its output or know when it finishes executing unless you use channels) or you join it with other sub-futures so that all your different work will progress simultaneously. Note that you can join multiple time, so you can start two futures, join them, and have one of those futures also start two futures and join them, there's no problem here.
In JS, for example, the `bluebird` library is a third party utility for managing execution of functions. You can do things like
const results = await Promise.map(users, user => saveUserToDBAsync(user), { concurrency: 5});
And I pass in thousands of users, and can specify `concurrency: 5` to know that it will be execute no more than 5 simultaneously.Implementation of this behavior in user space is trivial in JS, is it possible in Rust?
https://docs.rs/futures/0.3.0/futures/stream/trait.StreamExt...
Not only does the `futures` crate provide most things you'd ever want, it also has no special treatment – you can implements your own combinators in the same way that `futures` implements them if you need something off of the beaten path.
Additionally, you're making snarky comments about how you don't like how the base language doesn't handle something like JS...then reference a third party JS library. Base JS doesn't solve your 'problem' either.
To answer your question, async/await provides hooks for an executor (tokio being the most common) to run your code. You things like that in the executor.
https://docs.rs/tokio/0.2.0-alpha.6/tokio/executor/index.htm...
The primitive operation provided by a Rust `Future` is `poll`. Calling `some_future.poll(waker)` advances it forward if possible, and stashes `waker` somewhere for it to be signaled when `some_future` is ready to run again.
So the implementation of `join` is fairly straightforward: It constructs a new future wrapping its arguments, which when polled itself, re-polls each of them with the same waker it was passed.
There are also more elaborate schemes- e.g. `FuturesUnordered` uses a separate waker for each sub-future, so it can handle larger numbers of them at some coordination cost.
And from a quick scan of the source it doesn't look like anything there is impossible to implement in userspace: https://docs.rs/futures-util/0.3.0/src/futures_util/future/j...
The wording is a bit imprecise; you can't 'await' something to invoke it, exactly. It also won't begin execution until .await is called. What happens is, at some point, you have a future that represents the whole computation, and you pass it to an executor. That's when execution starts.
There's a join macro/function that works the same way as Promise.all, and is what you'd use for parallelism.
For instance, I use the CurrentThread runtime in tokio, because I'm using the rust code as a plugin to Postgres, and it accesses non-thread-safe APIs.
What you are asking for is essentially for the futures runtime to be hidden from you. That's fine for some languages that already have a big runtime and don't need the flexibility to do things differently, but doesn't work for rust.
If they are, then they're still not referentially transparent. But if they aren't then it might be a bit of a surprise to developers coming from other languages (especially ones not familiar with something like an IO monad).
Async is about interleaving computations on a single thread.
* First, it runs the callee synchronously until the first await, which can fire off network requests, etc.
* Second, continuations are pushed onto queues immediately- the microtask queue that runs when the current event handler returns, for example.
Rust does neither of these things:
* Calling an async function constructs its stack frame without running any of its body.
* Continuations are not managed individually; instead an entire stack of async function frames (aka Futures) is scheduled as a unit (aka a "task").
So if you just write async functions and await them, things behave much more like thread APIs- futures start running when you pass a top-level future to a "spawn" API, and futures run concurrently when you combine them with "join"/"select"/etc APIs.
This is actually possible to do by using async blocks instead of async functions. E.G. you can write this:
fn test() -> impl Future<Output = ()> {
// Run some things directly here
println!("I am running in the current frame");
async {
// Run some things in the await
println!("I am running in the .await");
}
}Think of it this way. I have 3 letters I need to send, and I'm expecting replies for each. A single threaded, synchronous language, would basically send the first letter, wait for the reply, send the second letter, wait for the reply, then send the third and wait for the reply. In JS, you're still single threaded, but you just recognize that there is no point in sitting around and waiting before moving onto the next item. So you send the first letter, and when it would be time to wait, you continue executing code, so you immediately send the next letter, and then finally send the 3rd.
How they're scheduled simultaneously on a single thread is exactly what makes JS so fast for IO. Once it starts making an http call, db call, disk read, etc, it will release the thread to begin execution of the next item in the event loop (which is the structure JS uses under the hood to schedule tasks).
So what really happens is when we call
asyncA();
asyncB();
JS will go into `asyncA`, run the code, and at some point it will get to a line that does something like "write this value to the database." This will be an asynchronous behavior, that it knows will be handled with a callback or a Promise, so it will immediately continue execution of the code. So now it pops out of executing `asyncA` and executes `asyncB`, meanwhile the call to the DB has gone out and we don't care if it has finished, we'll await both of these when we need them.This is usually false, in Rust and elsewhere. Futures are tasks that run on whatever thread is scheduling them. They are an even lighter-weight version of green threads/fibers/M:N.
More info: https://dev.to/gajus/handling-unhandled-promise-rejections-i...
The reason Rust does this is because its priorities are different than these other languages. Rust is built around zero-cost abstractions because it is intended to be as fast as possible while still safe. That's one of many reasons why Rust is considered a systems language and Javascript is not. They're for different things.
For more on how async in Rust works, I'd invite you to read the actual manual on the subject (linked from the announcement): https://rust-lang.github.io/async-book/06_multiple_futures/0...
In a language like Haskell where everything in lazy, it's not uncommon for someone to model their logic in such a way that computations that are not necessary are never run but appear to be used in code anyway.
Depending on what you want to do, it's also possible to start threads/green-threads (using something like Tokio), and use message passing for async tasks where you do not need to process the result synchronously.
I found that most code that actually use create_task tend to be super complex and often bug-ridden since eventually you will have to await it anyway, and it is easy to miss this, which will leave errors unhandled, especially propagating cancel() to all these executing futures floating in air.
Both will execute at the same time and you'll get a tuple of results.
Also, this has all the goodness: open-source, high quality engineering, design in open, large contributors to a complex piece of software. Truly inspiring!
This is a foundational implementation and while you _can_ use it, you are also likely to run into a host of partially implemented support problems. No fault of anyone, just a lot left to do. Examples being, you may run into needing async FS ops, so you bring in one of those libs. You may need async traits, so you bring in macro helper libs, you may need async DB interaction, so you check your db lib and hope they have support. Etcetcetc.
None of those are problems to stop you from using Async if you want. They're merely reasons you may not want to use Async quite yet.
Personally I've found Rust's development cycle to be such a joy, including manual threading, that I can get by happily without Async for the time being.
But, I'm not currently doing a lot of work where I'm dying to use greenthreads-like paradigms. Threading works fine in my web frameworks, db pooling, parallel libs like Rayon, etc. So because I've got all the power in Rust I need currently I just have no reason to use Async.
WITH THAT SAID.. use it if you want it! I just might not recommend it to people coming into Rust unless they explicitly need Async. It's bound to introduce a few - hopefully mild - headaches, currently.
There are crates that provide ready-made ones, and that will work for almost all cases, but it's another dependency that you have to evaluate and stay on top of.
It is entirely possible to do yourself, though. Last month, I dove into the details during a game jam. Not much of a game came out, but I did manage to get a useful async system up and running from scratch:
https://github.com/e2-71828/ld45/blob/master/doc.pdf (cf. Chapter 3)
On a semi-related note, any thoughts on how you could merge Async with non-Async code? Eg, I've got a large codebase that is not threaded but not Async. In the future, I might upgrade the web server to be Async and slowly start porting code.
I had planned/hoped that I could make my own Async/Thread bridge. Such that non-Async code would live in it's own thread, and I would make a special Future ask a Mutex in another thread if data is available. Taking special care not to lock the Future's thread.
The goal of course is to not have to rewrite the entire app's blocking code at once.
Does this sound stupid to you?
Traits are a big one for me. Though, I've seen good things with the Async Trait Macro libraries. So hopefully those do good.
It doesn't make this Async-Stable event any less big of course, not trying to downplay it. We can't really move forward without this, so I'm super happy to see it merged! I'm just hoping to manage expectations :)
Rust is the first language that is both truly good as a low-level systems language and also truly good as a high-level modern labguage at the same time.
To me, it's amazing to finally have another option aside from just C/C++.
Technically, I might have had some other options before, but rust gets it right.
I'm pretty sure the state of affairs for async programming is still a bit "different" in Rust land. Don't you need to spawn async tasks into an executor, etc.?
Coming from JavaScript, the built in event-loop handles all of that. In Rust, the "event loop" so to speak is typically a third party library/package, not something provided by the language/standard itself.
There are some differences, yes.
> Don't you need to spawn async tasks into an executor, etc.?
Correct, though many executors have added attributes you can tack onto main that do this for you via macro magic, so it'll feel a bit closer to JS.
https://github.com/prisma/prisma-engine/
Been working with the ecosystem since the first version of futures some years ago, and I must say how things are right now it's definitely much much easier.
There are still optimizations to be made, but IO starts to be in a good shape!
Basically we offer a code generator for typescript, migrations and a query engine to simplify data workflows. Go support is coming next.
Looks like a great project, best of luck.
As a rust noob, small question based on the example given: Why does `another_function` have to be defined with `async fn`? Naively, I would expect that because it calls `future.await` on its own async call, that from the "outside" it doesn't seem like an async function at all. Or do you have to tag any function as async if it calls an async function, whether or not it returns a future?
It's kind of like how if you declare a Python function containing the yield keyword, then the function returns an iterable rather than simply executing top to bottom.
In the same way that Python yield only makes sense from within an iterable, Rust's await keyword only makes sense inside of a Future. Outside of a Future, there'd be no concept of a yield. This is why the "outer" function must be declared async.
Now would you tag something as async if not required? Likely not - it just makes things more complicated. One exception is when you expect you need to modify the body of the function in the Future to make use of await, and you want to maintain compatibility
Note that marking a function as async is only syntactic sugar for a function that returns a future.
However, Rust Futures have a very different implementation compared to CompletableFuture.
I was wondering if a more pleasant approach would be to add a 'defer' keyword to return a future from an async call, and have the default call be to await and return the result (setting up a default scheduler if necessary). Requiring the await keyword to be inserted in the majority of locations seems poor UX, as is requiring callsites to all be updated when you update your synchronous API to async.
Excited for where this takes us! Can't wait for tokio 2.0 now.
Maybe the primary benefit is that its new and sexy
If you are talking about async/await:
- for concurrency it has been in the standard library for a couple of years. Also you can implement it as a library without compiler support.
- for parallelism, Rust is in advance. Nim has a simple threadpool with async/await (spawn/^), it works but it needs a revamp as there is no load balancing at all.
You can also fallback on the raw pthreads/windows fibers and/or OpenMp for your needs or even OpenCL and Cuda.
Regarding the revamp you can follow the very detailed Picasso RFC at https://github.com/nim-lang/RFCs/issues/160 and the repo I'm currently building the runtime at https://github.com/mratsim/weave.
Obviously I am biaised as a Nim dev that uses Nim for both work and hobby so I'd rather have others that tried both comment on their experience.
I like coroutines but thats mostly because they are not threads, they only switch execution on yield, and that makes them easy to reason about :)
idk about Lua, but afaik python's async is pretty much implemented on top of coroutines ("generators"). `await` is basically `yield`
> I like coroutines but thats mostly because they are not threads, they only switch execution on yield, and that makes them easy to reason about :)
pretty sure i've heard the same thing said about async io!
And that would leave the motivation to be performance gains by being able to reduce the amount of threads I guess
(And that it is cool of course :)
i guess the difference from normal threads (preemptive multitasking) is that you explicitly mark your "yield points" – places where your code gives control back to the runner – with `await` (cooperative multitasking). some believe that this makes async stuff easier to reason about, since in theory you can see the points where stuff might happen concurrently
honestly i'm out of my depth re: async IO, haven't used it all that much :/ but if you're comfortable with python and want to dig into the mechanism of async/await, i really recommend this article: https://snarky.ca/how-the-heck-does-async-await-work-in-pyth...
it's long, but i found it very helpful – it actually explains how it all works without handwaviness. at the end the author implements a toy "event loop" that can run a few timers concurrently, which really made it click for me!
I tried out Rust for a typical server-side app over a year ago (JSON API and PostgreSQL backend), and the lack of async-await was the main reason I switched back to Typescript afterwards, even though Diesel is probably the best ORM I've ever worked with. Time to give it a try again.
I heard it uses locks?
This is not on the same memory right? https://news.ycombinator.com/item?id=21469295
If you want to do joint (on the same memory) parallel HTTP with Java I have a stable solution for you: https://github.com/tinspin/rupy
If you want two threads running in parallel to concurrently access the same memory location you don't need synchronization if you only perform reads, and you need one if there is at least one write. Like in any other language (this comes directly from how CPU works).
The good thing with Rust is that you can't shoot yourself in the foot: if you can't accidentally have an unsynchronize mutable variable accessible from two threads: the compiler will show you an error (unless you explicitely opt out this security by using unsafe primitives, in which case the borrow checker will let you go).
The solutions range from mutexes to copy-on-write and more.
std provides some useful helpers like Arc and Mutex (Arc<Mutex<Type>> is always safe to use across threads, as the lifetime is managed safely by Arc and accesses handled by Mutex).
There are crates out there for things like lock free sync and other GC if you need it. Rust doesn't force you into a single world view.
So it can operate on "the same memory", and there are a whole lot of ways to manage it safely. The right tool for the right job, really.
There are ways to perform parallel work that is "safe", Rust solves this with the borrow/checker implementation, another clear "safe" way would be to make everything immutable as seen on Haskell, Ocaml, F# to where who cares about who gets to what first if the underlying thing will never change.
Mutexes and locks and all the other ways of doing parallel work that is "safe" isn't a primitive thats cooked in with the language.
So while I don't think what I said was incorrect re: Go vs Rust concurrency, perhaps I misunderstood OPs question.
> Mutexes and locks and all the other ways of doing parallel work that is "safe" isn't a primitive thats cooked in with the language.
I didn't understand OPs question as strictly features cooked into the language, so I was not saying that.
With that said, if you're not allowing for Mutexes for "safe things that are cooked into the language" I feel like you won't like Rust. This is an odd metric, though.
edit: words
I need a language with minimal runtime, good support for the C ABI and structure formats, decent macros, good ecosystem and community for mainstream programming needs, and support for async.
Where else can I find that whole package?
Go has too much of a runtime, GC, and doesn't support C structure formats very well (i.e. best case you need to copy/translate them to Go; you can't operate on them directly).
C++ can do it, but the details are ugly and the result unsatisfying.
Ada lacks macros and a mainstream ecosystem.
So... what do I use if not rust?
I can't imagine doing that project it any other language.
a bit too excited. GP isn't wrong, Go pretty much has the same thing and I have never seen so much fanboyism for a single feature ever in my career.
I don't get it, but that might be because I am a manager.
See my example where a pure-rust postgres extension can start a background worker, listen on a port, and handle concurrent network requests, multiplexing them over a single backend calling a non-thread-safe internal postgres API.
That's possible because rust has no runtime to get in the way, it can use C structs natively and seamlessly, tokio offers the CurrentThread runtime (which doesn't spawn new threads), and a bunch of other details that need to be done right.
Try doing that in a pure-Go postgres extension.