Announcing Tokio 0.1
tokio.rs
tokio.rs
Question for the more knowledgeable Rust folks: Does this code (the echo server example) not handle the situation where the port is already in use? What's the return type of the socket bind in that situation?
EDIT: Wow six responses in as many minutes. I think I've created a new objective measure of how popular a programming language will be!
This line tries to bind the port and returns a `Result` type which is then unwraped, which will cause a panic when the port already is in use.
> Does this code
So
let sock = TcpListener::bind(&addr, &handle).unwrap();
Here, the bind method [1] will return an error if it can't bind to the port. unwrap will cause a panic to happen.1: https://docs.rs/tokio-core/0.1.3/tokio_core/net/struct.TcpLi...
TcpListener::bind(&addr, &handle).unwrap()
Rather than raising exceptions, the paradigm in Rust is to return a Response item, which has an Ok and an Err subtype. unwrap() here assumes that the response is Ok and just gives that back the data returned, or crashes if its an error.
> It won't crash. It's a "clean" shutdown.
:+1:
The TCPListener::bind call returns a Result enum, which can have the values Ok(SomeType) or Err(SomeError). The compiler forces you to decide how you want to handle that, for the sake of brevity the example code calls unwrap() which assumes the result is Ok and will either return SomeType or else panic.
It's generally considered bad form to use unwrap() (and by extension panic) in production code, but for examples where you just want to show something it's fine.
bind would either return a Option type or a Result type, unwrap is defined for both.
edit: Others have looked through the docs and confirmed it's a Result
My view is that sample code such as this should be as idiomatic as possible and that means providing a sample demonstrating a typical real use case. So seeing "unwrap" in this context doesn't sit well with me.
In fairness, this isn't specific to Rust. I find sample code like this in many projects regardless of language. However Rust is billing itself as a safe systems programming alternative to C and C++. It would help its marketing efforts, in my opinion, by having more robust samples than the competition. And let's be honest: given the competition is C and C++, that's a low, low bar--and this comes from a guy who's about as big a fanboy of those two languages as possible.
Edit for more disclaimer: documenting Rust code is a real pleasure. The team has done a stellar job at making documentation an easy thing to do while writing the code. It's already better in most cases than many other projects I can think of.
That is, in terms of safety, this is the same thing as explicitly handling the error and then terminating the process. Which is the only real way you're going to handle this error anyway, unless you wanted some sort of retry logic. Which you might!
But for most example programs there's no easy way to handle errors further than just using expect() unless you want there to be more error handling code than actual useful example code.
The idea is that I expect this to be Ok/Some, and if it's not, please use this error message.
Here, the programmatic object is the grammatical object. That's what I meant by 'backwards'.
could_fail().or_panic_with('message') may be even better.
unwrap should be confined to test and temporary code. It somewhat makes sense in example code, but expect is better for that.
There are sometimes cases when you very easily know that the unwrap will never fail and if it does something has gone very horribly wrong, in which case you might use it.
Libraries should keep usage of unwrap/expect to a minimum. Applications can be more liberal with it, but they should try to use expect or better error handling.
Not everyone perfectly follows this, sadly. But most do.
This will print an error out already.
$ ./target/debug/tokiotest
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error { repr: Os { code: 98, message: "Address already in use" } }', ../src/libcore/result.rs:837
Printing out a _better_ error might be helpful, though, I'll agree :) You could use expect for that, which lets you change the message in the ''s easily.This really shows a fundamental tension though, in documentation. Is this example supposed to be demonstrating error handling? Or just get you going? Does adding more complex error handling distract from the point it's trying to teach? These are sort of open-ended questions.
As I said in the post you're replying to, a nicer error message would be a good thing.
Whoever maintains the server and runs the service is also my user, though, in the general case.
> As I said in the post you're replying to, a nicer error message would be a good thing.
Yeah - I don't mind if unwrap panics with a dev-oriented message as it's basically an assertion, but I guess I expected expect() (no pun intended) to give a more user-friendly error. Maybe the format of the panic! output could be changed to bring the message to the front and the technical details after that.
Obviously not if it's used for input errors (network failure). Crashing assertions are made for bugs, not input errors.
We can agree in the cynical interpretation of the laziness of programmers, but the mitigations in this case are so trivial, and the stakes so low, that focusing on unwrap as a point of contention is a poor use of energy.
If rust had that examples would be even shorter using ? Instead of try! or unwrap
`quick_error` uses a trait like that to determine the exit code of `main()`, allowing it to return either () or i32.
quick_main!(|| -> Result<()> {
// insert Result-returning code here
});
Alternatively it takes a few lines to bootstrap it by hand e.g. ripgrep uses this code to bootstrap: fn main() {
match Args::parse().map(Arc::new).and_then(run) {
Ok(0) => process::exit(1),
Ok(_) => process::exit(0),
Err(err) => {
eprintln!("{}", err);
process::exit(1);
}
}
}
https://github.com/BurntSushi/ripgrep/blob/master/src/main.r...http://rust-lang.github.io/book/ch12-03-improving-error-hand... shows how to refactor a project to improve its error handling as well, for something a bit less abstract.
Such error handling code is usually untested, which is another way of saying 'buggy'. It almost always swallows useful information, like the backtrace. It sometimes lets program execution continue in a messed up state, causing very strange and hard to debug errors later on.
Certainly Rust makes it a lot harder to mess up error handling code than the languages I'm used to but in general I'm definitely in the 'all exceptions fatal' camp.
* Rust prevents deadlocks
* You can't leak memory in Rust
* Rust prevents race conditions
* Panic is not safe
etc. In my mind, bringing up memory safety here isn't a red herring; the parent said this:
> However Rust is billing itself as a safe systems programming alternative to C and C++.
The way in which we are safer is memory safety, nothing more. And knowing that is crucial.
Rust code will have bugs. Rust code will have security vulnerabilities. Rust is not a panacea.
I know you want to not overstate rust's claims given recent articles, but I think you're actually underselling a little here. For example, a rust `enum` make it much easier for the compiler to enforce code correctness. It's hard to go back to similar code in C or Go once you've gotten used to `match`.
> prevents segfaults
Only outside unsafe blocks.
> guarantees thread safety.
But you just said you can have deadlocks and race condition, so... perhaps change the frontpage?
The page clarifies later on that the definition of thread safety in question is "threads without data races." The 16 words on the front page of the website are not the appropriate place to go into caveats and nuances.
> Only outside unsafe blocks.
This is just true of all of our guarantees. Given that the vast, vast, vast, vast majority of Rust code is safe code, I don't feel this is misleading. Do you say that Ruby doesn't prevent segfaults due to its C FFI?
edit: I'm just saying "don't lie". I shouldn't have to defend this view point and get downvotes for it.
1/ It says on the tin "prevents segfaults". It does not prevent segfaults in all cases, but OK, let's debate the other claim.
2/ It also says "guarantees thread safety". What is the Wikipedia definition of thread safety? It varies, so let's take the most common "freedom from race conditions."
Steve come and say Rust can have race conditions https://news.ycombinator.com/item?id=13376485 and that it's a common misunderstanding to think it prevents race conditions. Surely it would be great if the frontpage would not promote it!
Ergo the frontpage claim is false.
The wikipedia definition is pretty vague, it relies on a concept of "safe" that isn't defined there. It's acceptable to say that "data race safety" is "thread safety", though confusing. Rust's homepage does clarify what it means in the bullet points below that statement, so I wouldn't call this a lie. It may be misleading though, and this is a common misunderstanding as steve mentioned, so I submitted a PR to fix it https://github.com/rust-lang/rust-www/pull/685
In the punchline, "prevent segfaults" is framed in negative terms. If you put "provides memory safety" it will be a bit less evocative of specific pain, but will give a warm feeling.
Not too sure of the segfaults bit, feel free to put in your thoughts on that PR
Rust is a systems programming language that runs blazingly fast, guarantees memory safety and provides correct concurrency.
Rust is a systems programming language that provides uncompromised performance, convenience and security.
Rust is a systems programming language that enables unprecedented levels of performance, productivity and safety.
edit: I like (2) best
This is a relatively uninteresting quibble: the guarantees of any language only apply outside their equivalent of unsafe blocks (e.g. Python is memory safe... until you use ctypes). Pretty much everything has a way to FFI down to interact with the machine; in Rust, it's just called `unsafe`.
(One way to look at `unsafe` is that it's a tightly integrated FFI to another language called "unsafe Rust", and it benefits from having zero performance or semantic overhead.)
If a language frontpage is inexact I'll assume the rest of the website is inexact, simple as that.
Terminology matters.
As it cost me 20+ downvotes to make a rational conversation with you all, I will stop interacting with the Rust community.
As I mentioned in https://news.ycombinator.com/item?id=13377957, in the world of programming languages, the term "memory safety" means something specific.
D calls itself safe on its website, but it too has the ability to escape-hatch into C/C++.
Language websites putting forth subjective claims or claims based on definitions which may be subjective is totally normal:
Go says that it "makes it easy to build simple, reliable, and efficient software", which is totally subjective.
Ruby says "It has an elegant syntax that is natural to read and easy to write.", also subjective.
Python says "lets you work quickly and integrate systems more effectively", also subjective.
By the same token, you should be requiring that every subjective statement on the other langs' pages have the caveat "in the opinion of the $LANG developers." Obviously no one would wear that.
While teaching Rust it's a recurring theme among students to believe that all crashes are created equal and that an exit from a panic must be equivalent to a segfault, despite this being significantly false. So it's common when discussing panics to novice audiences to sprinkle in "by the way, this isn't a crash, it's a controlled exit".
Is it? It's out of bounds of any object, and therefore undefined behavior.
It's one of the more common ways of using unwrap.
(The examples unwrap elsewhere in less known-good scenarios, I agree that we should improve there)
I've starting trying to use `if let Some/Ok(...) = ...` in examples for my projects rather than `unwrap` for pretty much this reason. I feel like it strikes a pretty good balance between providing a relatively terse example without the risk of implying that `unwrap/expect` is considered normal usage for the library.
The result of parse().unwrap() is of type F, which means addr is of type 'F'. afterwards when we call `let sock = TcpListener::bind(&addr, &handle).unwrap();` the compiler can infer from the signature of `TcpListener::bind`[2] what F has to be, namely `std::net::SocketAddr`
[1] https://doc.rust-lang.org/std/primitive.str.html [2] https://tokio-rs.github.io/tokio-core/tokio_core/net/struct....
impl FromStr for SocketAddr
If you look at parse https://doc.rust-lang.org/std/primitive.str.html#method.pars... it says fn parse<F>(&self) -> Result<F, F::Err> where F: FromStr
So any type that implements FromStr can be parse'd from a &str.Due to the resulting thread, I feel legitimately excited to sit down and learn a new-to-me language for the first time in ages :)
My intermediate impression is: Basics and the whole idea behind it looks very good and suits Rusts general concepts. However from a productivity point of view it yet can't match the ergonomics of Go or C# with TPL and async/await for IO tasks. But I think the rust contributors are well aware of this and working on some things (async/await like sugar and that impl trait thing which might fix my composition problems).
More likely, you'll be using something like this: https://tokio.rs/docs/getting-started/simple-server/.
Perhaps something like that should also be on the homepage to show that, while you can do low-level, it has already scaled up to a higher abstraction for you.
let addr = "127.0.0.1:12345".parse().unwrap();
Is Rust like Ruby in that devs and library authors can add methods to and redefine built-ins? And that merely including a library can alter the definition of those objects everywhere in your project's code?I'm not trying to start a language war, but IMO that's too bad: this feature is, in my experience, the single largest source for difficult-to-detect errors that characterize the Ruby programming experience, e.g. anything that uses Rails. It also contributes to deep stack traces which are essentially useless, because the departure point from your actual code is some 20+ frames above the site of the error.
It's closer to refinements than monkey-patching, that is, you can, but you must bring the trait into scope for it to work.
> And that merely including a library can alter the definition of those objects everywhere in your project's code?
No, based on the above rule. We don't want that.
So, specifically. Rust has "traits", which, for these purposes, work like Ruby modules. But. Rust has "coherence rules." That is,
You can only implement a trait for a type if you defined either the trait, or the type, or both.
So, because libstd (well, Rust itself, but you get the idea) all three of FromStr, &str, and SocketAddr, it's allowed to do this. You, in your own package, could not define this, since you wouldn't have defined any of them.In tokio-core, they bring SocketAddr into scope: https://docs.rs/tokio-core/0.1.3/src/tokio_core/net/tcp.rs.h...
However, I was thinking of a slightly different case when I said that, which is methods. That is, you couldn't call methods unless they've had their traits brought into scope. But parse isn't on a trait; it's defined on `str` itself. In that definition, it also has to bring FromStr into scope.
So basically, this is very muddled by the fact that this particular example uses the standard library heavily, which already implicitly brings a lot of things (like &str) into scope for you. External packages don't have that.
It's unclear to me what you're saying here. It's true that you can't make an inherent method `parse` on str like the one being called here, it's also true that one couldn't implement FromStr for SocketAddr in an external package, but one can create a custom generic function that uses FromStr and have that generic type be SocketAddr for some call sites. Of course, if this function wanted to have the syntax of a method call on str, it would have to be defined in a trait that users then import.
> In tokio-core, they bring SocketAddr into scope: https://docs.rs/tokio-core/0.1.3/src/tokio_core/net/tcp.rs.h....
To be clear, this isn't necessary for calling methods---including inherent methods on SocketAddr---unlike the methods in a trait. The only reason to do this is that it lets one abbreviate the name: "SocketAddr" instead of "::std::net::SocketAddr".
In this case, both of those methods are in the standard library.
From reading the docs, something provides a method called `FromStr` and the `str.parse` method does some magic to figure out which `FromStr` of all the registered traits is the one which we want? So then, it's not arbitrary monkey-patching, but using a defined protocol for defining and extending string parsing?
> From reading the docs, something provides a method called `FromStr` and the `str.parse` method does some magic to figure out which `FromStr` of all the registered traits is the one which we want?
The method isn't doing any magic: the language's type inference is deducing that the return type has to be a SocketAddr based on how the return value is used (it is passed to a function expecting a SocketAddr), and this forces the compiler to use the SocketAddr implementation of the FromStr trait (which has a from_str method), all at compile time.
(If the compiler deduced that the return type was something that didn't implement FromStr, i.e. no idea how to parse that value from a string, one would get a compile error.)
> So then, it's not arbitrary monkey-patching, but using a defined protocol for defining and extending string parsing?
Yes, parse is entirely driven by the FromStr trait.
It was announced roughly in August, so yeah, five months. Though some work had to happen for that initial release, of course. There was a joke though, about this rapid development. Someone said something like "That they keep re-writing tokio is really annoying as a user, but I guess if Rust is so productive that you can throw out huge chunks and re-build it that quickly, well, that says good things about the language." Of course, as the post says, things will be more stable now.
#[derive(Aws!("s3/2006-03-01/service-2.json"))]
struct S3;
Becomes up-to-date with the latest changes from Amazon the moment they're released (recompile required).Procedural macros are really exciting with regard to writing/consuming APIs as they enable both the client and the server interface to be implemented in an API specification language (Amazon uses custom JSON, but Swagger/RAML could work too) instead of Rust with zero performance penalty (gotta love those zero-cost abstractions :-)
We're tracking the missing services: https://github.com/rusoto/rusoto/issues/436 . There's plenty of work to do and we're concentrating on getting them implemented. Sometimes progress is slow since it's a side project, but it's still moving forward.
If we can make the derive statements work as your code snippet shows, I'd be really happy. We'll take a look at how we can improve codegen when new features are available. Some of our codegen is dated, using what was available when it was written.
It seems like many things are in flux. There's a new `rest_xml` codegen thing that just got implemented which will allow AWS API to be supported. They're also moving from Hyper to Reqwests. From what I saw, Async stuff is planned for after 1.0 by adding new AsyncClient impls backwards-compatibly.
We also have a crate that's designed to provide higher level abstractions. Rusoto is analogous to botocore and rusoto_helpers (https://github.com/rusoto/rusoto/tree/master/helpers) will be closer to boto3. It's on the back burner as we focus on completing the core functionality.
Async support is still an open question. If requested we can make that happen before 1.0. Feel free to weigh in: https://github.com/rusoto/rusoto/issues/318 .
Overall I was super happy. I finished the feature I was working on in maybe a day or two without any issues in the rusoto library. Can't ask for more than that.
One of the core premises of tokio is that this compiles down to the state machine you'd have to write by hand if you wanted to do asynchronous stuff. So yes, this is very much intended as a zero-cost abstraction.
Then stuff like spinning up 1 OS-thread for every CPU core and scheduling requests in round-robin (?) style, and then wrap it all up into a developer-ergonomic thing.
Since you probably have a way better insight into rust-dev and the ecosystem, can we stay tuned that this eventually will happen or are there still showstoppers somewhere?
You could see how that match block functions as a router...
I was literally playing around with this last night. There's a lot of experimentation going on in the server-side Rust web framework space right now, and I expect it to heat up even more now that tokio has had a release.
> once Macros 1.1 land in stable, right?
So to be clear, macros 1.1 gets you custom Derive, which is Serde/Diesel. It'll be stable in the next release in ~3 weeks. It won't get you the full ability to use any custom attribute, like https://rocket.rs/ uses.
> Then stuff like spinning up 1 OS-thread for every CPU core
This is not implemented in tokio yet, but in my understanding, it's coming.
This alone are awesome news! Having worked with literally dozens of web frameworks, I have a sincere interest to see what emerges from the rust space. I can't pinpoint or prove it, but many developers seem to care about doing stuff "the right way" instead of getting something out the door that works maybe Ok. I really wait for the "this is it"/"we couldn't possibly solve this better" moments that might arise from such experimentations.
> macros 1.1 gets you custom Derive, which is Serde/Diesel
... basic building blocks for all things web I'd say. Looks like the next stable release will arrive just-in-time for things to emerge.
And then there might be the next wave of rust-users/developers that want to built upon such rock-solid foundations, like me. Really, I'm excited like I haven't been in many years!
> This is not implemented in tokio yet, but in my understanding, it's coming.
I just started full-time work on something like that (waiting out a noncompete) but more similar to the LMAX disruptor than a round-robin scheduler. That sort of heavyweight pinned actor architecture has a lot of performance and simplicity benefits especially for something like Rust which already has a solid handle on good memory use.
#[derive(Serialize)] <- you can write anything inside the ()s
Full on procedural macros, attribute style: #[get("/hello/<name>/<age>")] <- you can write anything inside the []s I enjoyed visiting Tokio (Tokyo) the city and I liked the
"io" suffix and how it plays w/ Mio as well. I don't
know... naming is hard so I didn't spend too much time
thinking about it.
[0]https://www.reddit.com/r/rust/comments/4vzomj/announcing_tok...Don't even get me started about learning French words in either of them and then learning their pronunciation in the other, and then the actual French. As an example, the Atelier game series.
とうきょう 「漢字:東京」 is the capital. 「ときょ」is gibberish and doesn't exist.
(.rs being TLD of Republic of Serbia)
Romanization is a way of unambigously encoding Japanese into the roman alphabet. One method gives us "toukyou". Another system uses diacritics for long vowels.
"Tokio" is a different word.
tokio -- ときお (to ki o)
toukyou -- とうきょう (tō kyō: this is Tokyo)
tokyo -- ときょ (to kyo)
Note that ō loses an orthographic nuance, since it can denote either おう or おお.
If you're going to be strict with respect to romanization and use accents, isn't the circumflex used to indicate long vowels, at least in Kunrei? So, Tôkyô? Or in Hepburn, the macron? Tōkyō? I've never come across acute being used for this.
Edit: By the way, your idea of "unambiguously" encoding Japanese doesn't hold water. Written Japanese is written in a mixture of kanji and kana, and no romanization system does round-trip the kanji. You could say that (good) romanization systems unambiguously encode the pronunciation, but that isn't true either: in addition to the fact that encoding "pronunciation" of written language is problematic in principle, there are the pitch accents that aren't captured by kana. But we're digressing.
If you give up the "unambiguous", "tokyo" is a perfectly good romanization for 東京.
And by cheap I mean cheaper than OS threads. The reactor pattern still adds a lot of CPU overhead to each I/O operation.
What's interesting is that Rust had green threads at one point. They implemented that by making std::io async-capable under the hood. But they didn't like it: too much abstraction cost, and it was preventing to add more native i/o features easily.
So they ditched it and now they are doing exactly the same thing to I/O, just in user space, and without green threads.
You need to pay for concurrency one way or another, there is always going to be some bookkeeping overhead. Whether you pay the price in your language's runtime (like Go) or in a library doesn't mean you're getting for free. However, it looks like the design of Rust's futures will let it reach very throughput.
I've measured "zero-cost abstraction" that weren't quite so when profiled.
What it really means is "if the inlining goes like the programmer expects, then it should have no cost". The term expresses hope.
But inlining is best left to the compiler, and hope is of little value until measurement is done with a profiler.
But you don't necessarily want to do that since you may not make the most efficient use of the CPU cache.
[1] https://doc.rust-lang.org/reference.html#inline-attributes
It was cool and trendy when libevent and Nodejs came out, but now we should all have the experience and knowledge that tell us to stop doing asynchronous I/O.
Go, Hakell, or Erlang do a great job at concurrency, without the async i/o craziness.
I can appreciate a polemic, but I also appreciate substance ;)
Async code can not use synchronous code because this would block it, and prevent it from returning to the event loop.
This is a tedious task, and you end up with less tested, less complete code (at least during the first few years) compared to the sync libraries provided by vendors and std libs.
Event Nodejs still doesn't have ported the world to async yet, and still uses a thread pool under the hood for a number of things (e.g. name resolving), which defeats the promises of async I/O.
Synchronous code can not use asyn code either because, well, in order to get anything from async code you have to be async yourself.
Async code is also more difficult to reason about compared to classical blocking code.
The idea behind async code is to avoid the cost of context switches and the memory usage of OS threads. But they are not the only way to avoid these costs. Go, Erlang, Haskell do a great job at this, without forcing the world into async.
And since this guy is way better than me at writing, I'm going to leave this url here : http://journal.stuffwithstuff.com/2015/02/01/what-color-is-y...
In Go, you can decide that a call frame and all its descendants will live their own life in a separate thread of execution. But inside of that call frame, the code is just usual, synchronous code. It doesn't event need to know that it's a goroutine. It is just executing concurrently to other code, at a very low cost to the computer and to the programmer.
Go doesn't have async i/o by choice, it just doesn't need to. Though I'm pretty just there must be a libevent or libuv binding somewhere.
I think you misread the gp. He was not saying that Go code is not different from async code (what you understood), he said it's not different from synchronous code using threads.
I agree that there is no visible difference in the code, and that's exactly what I like. What's different, however, is how Go routines have much less overhead than OS threads.
We're definitely aware of all of this. Tokio is made by two Rust core team members and the person who wrote the most widely used async io tool; it's virtually all but provided by Rust itself. And the ecosystem is aware of this too; the other people who were working on AIO have backed tokio as well, and the Rust community in general is interested in not having this split.
> Synchronous code can not use asyn code either because, well, in order to get anything from async code you have to be async yourself.
This is solvable through a threadpool, which tokio provides. In other words, it lets you make a blue function red.
> But they are not the only way to avoid these costs
These do not avoid all costs. For example, they pay the overhead of green threads, which means that you can't interoperate with C code at zero cost. That's a price a language like Rust cannot pay.
How will you avoid having two variants of each and every lib ? E.g. redis-rs, sync, and redis-tokio, async ?
> you can't interoperate with C code at zero cost
Go can't because it has different a memory layout and calling conventions.
Other than that, what are you thinking about exactly ?
If the async version is easy to use, why would you use an explicitly synchronous one?
> Other than that, what are you thinking about exactly ?
Green threads. To call into C, you need to switch stacks. This is (one of the big reasons) why we removed green threads from Rust.
Having a sync interface would make it easy to use in sync code, yes. I feel that we are far from zero cost abstractions now, though.
> switch stacks
Is it because the stacks in green threads is smaller than what C would expect ?
An interesting approach taken by Go here is to avoid calling C as much as possible. They don't call the libc for system calls, for instance. This is also what allows them to switch the execution to an other goroutine just before the syscall.
Why?
> Is it because the stacks in green threads is smaller than what C would expect ?
Yes.
> An interesting approach taken by Go here is to avoid calling C as much as possible.
Right, so this is a cost that they can pay. Rust cannot.
You had a call to read(). Now you have a thread, an event loop, a pooling mechanism, and a synchronization with the thread. That's much more system calls and cpu cycles, for an synchronous async read()
When you make a synchronous syscall, the kernel initiates the operation, saves the state of your thread, and starts another one. When the network interface is done, it signals the kernel, which then marks your thread as runnable and schedules it for execution.
When you make an asynchronous syscall, the kernel initiates the operation but does not block your thread. This is usually done in the context of an event loop, which makes a synchronous syscall (like epoll_wait) when it runs out of tasks to run.
Thus, converting a single async syscall to a sync one means two syscalls: initiate the operation, then wait for its result. The extra round trip between user and kernel mode is basically free in this case because you're blocking on IO, and any logic it implements has to happen in the synchronous case anyway.
You can not say that there is NO overhead. Using async code in a synchronous fashion is not going to be as fast as using plain sync code.
Look at the wait() function: https://github.com/alexcrichton/futures-rs/blob/master/src/f...
All this code IS overhead that wouldn't exist if the code was synchronous in the first place.
https://github.com/alexcrichton/futures-rs/blob/master/src/f...
Of course depending on your executor, the IO can be async or sync.
Have you ever ran a benchmark of a wait(async_read()) compared to just sync_read() ?
Do you mean a human/development cost ?
Other than that, what would rust lose for not using the libc for system calls ?
Go and Rust don't have the same goals. Rust has been designed as a replacement fro C and C++, that can be progressively integrated in a existing code base (like Firefox's one, or librsvg's). Go is Google's replacement for Java and Python to build independent micro-services.
Go does a great job in its niche, but won't work at all where Rust shines. They are different languages, meant for different use-cases and if people could stop comparing them every time one is mentioned, I think we've made a great step forward …
Are they really that different use cases? Only rust is aimed at systems programming, but it seems like it could fill the application programming role quite well, where it is competing with go.
Why do you say that? Taking an async zero-cost-abstraction API and calling wait() on it doesn't magically make it more expensive. It just blocks the current thread until the async operation is done. Said operation is just as fast and zero-cost as it was before.
Obviously that's not the case. There is a lot of overhead.
By having one, the async one. If someone doesn't care about asynchronicity, there's always some sort of "wait until completion" functionality. (This is practically what the "normal" synchronous IO functions are doing anyway, just internally.)
The python community is also struggling with this problem. The emerging approach, which seems to me to be the right approach, is to write the libraries such that they do not do any IO; then you can use them anywhere, with some integration. So basically it's just the principle of separation of concerns.
See more here: https://sans-io.readthedocs.io/how-to-sans-io.html
I'll add that once you have this, the async/sync distinction (functions that return Future and those which don't) in Rust becomes exactly the same as fallible/infallible (functions that return Result or don't) and gets handled pretty much the same way.
You're not avoiding the cost, you're just moving the runtime and language/code complexity costs around. Each of the techniques used by Go, Erlang, and Haskell to implement coroutines have trade-offs and there is two that Rust simply cannot make and still fulfill its goals: lose low overhead bidirectional C interop and add a language runtime.
Performant coroutine implementations (AFAIK) all require moving the stack pointer around which makes it very expensive to have code call FFI functions. C makes certain assumptions about the stack and invariants need to be upheld, especially when the foreign library takes a function pointer from the host language. These features require a runtime which is out of the question for a low level language.
This sounds like a JS-specific issue, where you're not allowed to spawn new threads for historical/implementation reasons? Even in Python and Ruby, where the GILs prevent threads from really running in parallel most of the time, you can still use them to unblock an event loop around a long-running function.
You are writing async code to avoid threads. If you bring threads in your async code, you get the worse of both worlds: Convoluted code, and thread issues (pool exhaustion and/or thread overhead).
writing async code to avoid threads
Async can be used in single-process systems.
It can make sense to use async techniques to maximize work being done on each process/thread. Working with threads and using async methods can both be complex if you don't have good abstractions for doing so. They can be used together to great effect if you have the right tools.
> You can only call a red function from within another red function.
That's really really true in JS. There's no way to call an async function from a sync function, if you need to return its result. You are Capital-S-Screwed if you need to do that.
But it's going to far to apply that absolute rule to other languages. When you have threads, it's pretty easy to mix sync and async code. It can be a bad idea, just like having a codebase that's half exceptions and half error returns is usually a bad idea, but you're certainly allowed to do it when it makes sense.
So you have to rewrite every single library and network client in asynchronous mode.
It would be great to have every I/O library use OS specific polling mechanisms but that places a significant burden on library developers to not only know their domain, but have experience with each platforms quirks and limitations.
Until you exhaust the thread pool, in which case everything relying on the thread pool will be much longer than usual to complete.
Nodejs does this for name resolving, and it's a nightmare. Basically, with the default pool of 4 threads, it suffices of 4 slow resolves to DoS everything that needs to do a name resolve in the same process.
I hope that Rust will not have the same problem when mixing async and thread pools.
If the thread pool is not limited in size, you avoid this problem, but you lose all the benefits of async.
You can't have both: either you have a limited pool and can block it on long running tasks or you don't and can wind up with a large number of threads. Go currently doesn't give you this choice and you're limited by whatever you set GOMAXPROCS to before you start your program. If you have GOMAXPROCS set to 4 and you have 4 goroutines that take a long time (not waiting for IO) you've blocked the ability to do any other work. This isn't entirely true of course because they have a runtime in-process scheduler (which adds more overhead) but you could easily avoid this particular problem with tokio by using an async DNS resolution solution, which is what Go is doing in their stdlib for you.
Like with a lot of Rust development at the moment, these libraries are building blocks, they're not the end of the story.
There is just no need for async I/O. Just bring "zero-cost" multi tasking / coroutines to your language, and you don't need async anymore.
They do this by using async IO under the hood (for network sockets at least- convincing disk-based read() calls to be async is much tricker and Go doesn't do it).
That's why Go, for example, does use async IO. But it makes the use of it look like sync IO for simplicity. In the runtime, however, it is doing all the async event management so that you don't have to.
From a programmer POV this is a good thing, but it comes at the price of a runtime that has to exist. Something that Rust eschews.
The OS kernel itself provides both blocking and asynchronous syscalls. So if your OS-level thread is switching between several coroutines, and one of them makes a synchronous read() syscall, the whole OS-level thread blocks and can't run any other coroutines until the read completes. If it instead makes an asynchronous syscall, the kernel returns immediately and the OS-level thread can switch to another coroutine while the first one waits.
I couldn't tell the same thing about Nodejs, for instance, which is why I tried Go in the first place.
Incidentally my code is much easier to understand, too.
nodejs use to be pretty annoying when dealing with complex workflow in term of code legibility when it was fully callback-based, but with async/await you get the best of both world IMHO.
And it's still less natural than writing sync code
Being a green thread with a growing stack is an implementation detail for the developer writing Go code.
In async, every single function must be async (or be super fast and i/o-free).