I can appreciate a polemic, but I also appreciate substance ;)
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.