Hacker News new | past | comments | ask | show | jobs | submit login

Can someone explain to a non-rustacean what Tokio introduces that's not part of Rust? It looks like Rust provides the async/await semantics, so I'm guessing this is an event loop and dispatching system?



The Rust language provides the async/await syntax, which can turn imperative code into Future objects, but to run those Future objects, you must repeatedly call their poll method. Tokio is the piece of code that calls that method. It does so in a manner such that futures are only polled if able to continue work, and not e.g. waiting for a timer.

Besides that it provides lots of utilities for working with async code.


Would it be fair to compare this to Apple's Grand Central Dispatch a.k.a. libdispatch?


It's similar. libdispatch is primarily a task queue to simplify and optimize multithreaded workloads in objc/swift. Tokio uses a similar task queue pattern for scheduling cpu-bound or blocking io, but the main feature is an non-blocking/asynchronous IO runtime. Overall, it's more similar to libuv (the async io library that powers node.js), but with built-in support for Rust's async/await syntax


You mean io-bound


Yes in some regards. Both offer eventloops (in GCD: dispatch queues), which run small chunks of code which belongs to independent tasks on the same thread.

However there are some differences:

- Tokio is focussed on running async/await based code, whereas libdispatch currently mostly targets running callbacks/continuations. This might change once Swift offers async/await support, which for sure could run on top of GCD queues.

- GCD queues provide a lot more fine-grained control. users can exactly specify on which queue to run some code on. And tasks can jump between code. There might also be a dedicated main thread (UI) queue. Tokio just spins up a single queue which runs all code, which might be empowered by a multithreaded executor. This makes it less usable for UI.


Yes, but it's strictly userspace. It's more like the concurrency primitives in the JVM. (Eg. work-stealing threadpool, timers, various abstractions over OS/kernel lower level async stuff.)


To me it's kind of like the event loop of node.js (which I believe comes from libuv)?


Yes, Deno uses it as its internal event loop for JS code.


Fun party fact: long ago, Rust had libuv built in too.


... for not-so-fun parties. ;-)


why’d they move past it? does libuv not fit well in rust?


Rust used to have a lot of stuff built into the language, including garbage collection![0] I think that a long time ago they chose to move stuff out into libraries so that Rust could be a serious competitor to C++.

https://pcwalton.github.io/2013/06/02/removing-garbage-colle...


Back then, Rust had a built-in runtime. It was removed, and libuv with it.


> but to run those Future objects, you must repeatedly call their poll method

Oh man. C++'s std::future has the same silliness. I've come to the conclusion that futures/promises are a dumb abstraction.


What about this bugs you? You know that it’s not like, a busy loop calling poll all the time, right?


Well, I took the phrase 'repeatedly call' to mean pretty much that!

What bugs me about it, if I'm understanding it correctly, is that you have two options once an async call is issued:

- the calling thread effectively waits for completion. This is fine if a fork/join pattern is useful to you (i.e. issue N async calls and then wait for N completions). This isn't proper asynchrony though.

- the future is pushed on to a thread that does nothing but poll for completions. This effectively imposes an O(n) inefficiency into your code.


I can't speak to the same level of depth about the C++ model as the Rust one, but, while you could do those things, it's not the usual way that it works, at least, if I'm understanding your terms correctly. I'll admit that I find your terms in the first bullet pretty confusing, and the second, only slightly. Let's back up slightly. You have:

* A future. You can call poll on a future, and it will return you either "not yet" or "done." This API is provided by the standard library. You can create futures with async/await as well, which is provided by the language. These tend to nest, so you can end up with one big future that's composed out of smaller futures.

* A task. Tasks are futures that are being executed, rather than being constructed. Creating a task out of a future may place the future on the heap.

* An executor. This is provided by Tokio. By handing a future to Tokio's executor, you create a task. The job of the executor is to keep track of all tasks, and decide which one to call poll on next.

* A reactor. This is also provided by Tokio. An executor will often employ a reactor to help decide which task to execute and when. This is sometimes called an "event loop," and coordinates with the operating system (or, if you don't have one of those, the hardware) to know when something is ready.

* A Waker. When you call poll on a future, there's one more bit that happens we couldn't talk about until we talked about everything else. If a future is going to return "not yet," it also constructs a Waker. The Waker is the bridge between the task, the reactor, and the executor.

So. You have a task. That task needs to get something from a file descriptor in a non-blocking way. At some point, there's a future way down in the chain whose job it is to handle the file descriptor. When you ask it to be created, it will return "not ready", and construct a waker that uses epoll (or whatever) via the reactor. At some point, the data will be ready, and the reactor will notice, and tell the executor "hey this task is now ready to execute again," and when some time is free, the executor will eventually call poll on it a second time. But until that point, the executor knows it's not ready, and so won't call poll again.

Whew. Does that make sense? I linked my talks in this thread already, but this is kind of a re-hash of them.


This is an awesome rundown of the whole stack. It's almost like you've explained this stuff before. ;)

It might sound complicated, but for typical applications almost all of this happens "under the hood". Usually you'll just add an attribute to `main` to start your runtime, then you can compose/await futures without ever needing to think about `poll` and friends.

Here's a small example: https://tokio.rs/tokio/tutorial/hello-tokio.


Thanks :)


In very loose terms: Rust's async/await syntax defines an interface to async programming, not an implementation.

Rust leaves the implementation, and thereby choice of concurrency strategy, to libraries. Tokio is such a library. There's at least one other popular one of note.


> There's at least one other popular one of note.

The other popular runtimes are async-std [0] and smol [1].

[0]: https://github.com/async-rs/async-std

[1]: https://github.com/smol-rs/smol


if I understand correctly, smol was created by the async-std folks, and async-std now uses it as its runtime

https://github.com/async-rs/async-std/pull/757


From what I've read, smol was created by its author and later on adopted by async-std. And, its author was in Tokio team, and later left and was in async-std team before he left and created smol. So smol was based on quite deep knowledge and experience in async.


AFAIK, the major component is the scheduler.

Rust can't really implement green threading (imagine Golang) by default, because it requires the runtime to be bundled in the executable, and the code to be compiled in a specific way to be managed by the scheduler.

I actually find amusing the thought that _this_ is systems programming. In a low level language one can implement the functionality of a higher level language (ie. Golang), but not the reverse :-)


Both Rust and Go used to have green threads i.e. stackful coroutines / fibers that installed their own execution stack and switched to it.

However everyone who ever did green threads ended up having a lot of difficulties with stack optimization: - Go: https://docs.google.com/document/d/1wAaf1rYoM4S4gtnPh0zOlGzW... - Rust: https://mail.mozilla.org/pipermail/rust-dev/2013-November/00... - Same for Java in the past.

Now only Go is left.

More on fibers woes from the C++ point of view: http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2018/p136...


Replying to myself, I found this bit in the docs explaining how Tokio decorates the main:

> An async fn is used as we want to enter an asynchronous context. However, asynchronous functions must be executed by a runtime. The runtime contains the asynchronous task scheduler, provides evented I/O, timers, etc.


To provide some more color on why this isn't built in, different runtimes provide different kinds of guarantees and performance profiles. A webapp has very different requirements than an embedded system, and so we don't want to provide a single runtime. The language contains the basic things needed for the ecosystem to exist, and interoperation points, and then leaves the rest to said ecosystem.

(Some of those interoperation points are still being worked out, so it's not perfect yet.)


When learning Rust a few months ago, I built a small client library for a REST API using reqwest (which uses Tokio). I then started writing a web app using Tide (https://github.com/http-rs/tide). I eventually realized that it would be difficult to use the library I had built earlier since Tide uses the async-std runtime rather than Tokio. That was very disappointing. Is there any plan to make it easier to write "runtime agnostic" libraries in the future?


Yes, that is what I alluded to at the end. There's a few points here that still need some interop work. The intention is to fix that, but it's non-trivial. We'll get there.


Steve, just want to say thanks for your dedication - I find you’re always around when there’s a Rust thread on HN


You’re welcome!


You can make libraries runtime agnostic, but it requires a bit of design to get right. We tried our best with Tiberius[0], so you just need to provide an object implementing the AsyncRead and AsyncWrite traits from the futures crate, such as the TcpStream from async-std or tokio (using their compat module).

It is not perfect yet, and especially how tokio does not follow the rest of the ecosystem by implementing their own traits is kind of disappointing. We can work around that, but I was hoping they would fix this by version 1.0...

[0] https://github.com/prisma/tiberius


Yup, building libraries on IO traits which then are implemented by the particular runtimes is a good way to have a runtime agnostic library. Ideally the IO traits are defined in the library itself, to make them not again dependent on another moving target. You can provide implementations of the "glue code" for particular runtimes in separate crates to ease integration for users.

Another way to be runtime agnostic is to start a particular runtime as part of your library, which is used internally. The public interface of your library can provide async functions which are agonstic to a particular runtime, since all actions will be deferred/forwarded to an internal runtime. That approach has a bit more overhead, but can ease usage.


I had the exact same experience. I think it can be a pretty big barrier to getting started, as you really have to lock into a sub-set of the ecosystem (i.e. I can only use crates that have support for Tokio).

I understand the reasoning for not wanting this in the core language, but perhaps there could be some standard implementations which would still allow for custom runtimes.


You can make async-std mimic a tokio runtime by adding “tokio02” or “tokio03” to the list of features for async_std. At its core, futures and future combinators work under all runtimes, it’s just some features will complain if they don’t detect the tokio runtime.


that's a good point, when playing a bit with rust last year I found that the libraries that deal with "async stuff" (like http clients, db clients etc) are mostly split, some use tokio and some use async-std, and they were incompatible. Not sure if the situation has improved now but it looked like an ecosystem split at the time.


Work is in progress, but the situation hasn't improved yet for end users.


I knew you needed to use a library like Tokio with asynchronous Rust, but I never understood why, so thanks.

That said, the last time I looked at it, Tokio was much too complex for my tastes.


As always, very elegant. Thanks for the added info.



Yeah that's exactly right, it's an event loop.

It's hilarious to me that it's hitting "1.0" now. I remember the original release ~4 years ago?? That was way before Rust even had async/await. I imagine there were quite a few refactors. I mean, so much work, to make a library for asynchronous network service programming? If I needed an event loop and a scheduler I think I would just invest in implementing it myself, tailored to the requirements of my project.


Tokio is basically the asynchronous standard I/O library for Rust.

Async/await are language features. Tokio is the library for using these to do I/O.

Rust has a really tiny standard library (by modern standards) and a very high standard for moving stuff into the standard library. Right now it has no "standard" asynchronous I/O library ("async-std" bequeathed that name on themselves; it doesn't ship with Rust).


tokio is to rust what asyncio is to Python.


And nodejs or web eventloop is to JavaScript. They provide the executor and the timeout/IO primitives.


except say asyncio is pet of standard python


and tokio is pet of rust... I don’t see the value of this argument. Tokio is known as being THE async executor in rust. Asyncio being THE executor in Python. In other languages there’s a number of frameworks to choose from. In this comparison there really is only one.


> rustafarian

The community prefers the term "rustacean" to be as inclusive as possible. Please keep that in mind in the future.


TIL, thanks for the correction


(It's a combination of not wanting to trivialize a religion, and that 'RESTafarian' has a long history of use, and that's just confusing)




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: