Besides that it provides lots of utilities for working with async code.
https://pcwalton.github.io/2013/06/02/removing-garbage-colle...
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.
Oh man. C++'s std::future has the same silliness. I've come to the conclusion that futures/promises are a dumb abstraction.
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.
* 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.
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.
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.
The other popular runtimes are async-std [0] and smol [1].
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 :-)
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...
> 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.
(Some of those interoperation points are still being worked out, so it's not perfect yet.)
If you want to dig deeper:
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.
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...
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.
That said, the last time I looked at it, Tokio was much too complex for my tastes.
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.
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).
The community prefers the term "rustacean" to be as inclusive as possible. Please keep that in mind in the future.