But in Rust you can just spawn threads, share data through channels or mutexes, use OS-provided async IO primitives to poll file descriptors and do event-driven programming etc...
I tried looking into Tokio a little while ago and I found that it led to some incredibly complicated, abstracted, hard to think about code for stuff I usually implement (IMO) much more simply with a basic event loop and non-blocking IO for instance.
I'm sure async can get the upper hand when you have a huge number of very small tasks running concurrently because that's generally where OS-driven parallelism tends to suffer but is it such a common scenario? That sounds like premature optimization in many situations IMO. Unless you actually think that async code is more expressive and easy to read and write than basic event loops, but then you must be a lot smarter than I am because I have to take an aspirin every time I need to dig into async-heavy code.
I guess I'm trying to understand if it's me who's missing something, or if it's web-oriented developers who are trying to bring their favourite type of proverbial hammer to system development.
The upside of async over a simple event loop is, in my experience, when things become less simple, and you end up with hard-to-read little state machines all over the place.
With async, you can have your event loop, but the state machines are handled by the compiler. Code is like threaded code. That can be very convenient.
Threads, obviously, accomplish the same thing, and arguably more easily. But threads have a performance problem when they must interact heavily. Cross-thread communication is expensive. Single-threaded async task interaction is very cheap, comparatively (I use tokio only in single-threaded mode, as an event loop replacement; its multi-threaded scheduler performs terribly on serious I/O). I think the interaction problem is often more important than just the number of tasks.
As for the function coloring argument, I've started to see the async keyword as documentation. A non-async function is "regular logic", it must complete without blocking. An async function is a state machine; as such it must be part of a larger state machine (the event loop), and it can go down into smaller sub-state machines (i.e. call other async functions). If you make that distinction explicit, it all makes a lot of sense.
This all depends on how threads are implemented. If they're scheduled preemptively then communication can be expensive, relatively speaking, because of the need for locking and atomic operations. But you can also schedule cooperatively in user space, just as Tokio does when serially resuming async tasks; or as Java's Project Loom does for its new "lightweight" threads.
Note that unlike JavaScript, Tokio and Project Loom can also run different tasks on different, preemptively scheduled threads. And while I don't know that much Rust, I imagine you're going to need to use either unsafe or Rc or maybe even Arc if you intend to share data between different Tokio tasks--i.e. data that doesn't fit the normal caller/callee borrow semantics.
The other part of the problem is space requirements. Usually where you have preemptively scheduled threads the stack space for a thread is allocated lazily as a function is called and faults in pages via the OS' virtual memory system, much like single-thread, single-stack processes in a preemptive process OS. This means the minimum space allocation for a thread is at least 2x the page size (e.g. 4096 * 2). But many times a thread of execution only goes a couple of function calls deep, with minimal amounts of function-local (i.e. stack-allocated) data. If you have 1 thread per network connection, with hundreds of thousands or millions of connections that overhead could be significant.
But this, too, is a function of the implementation. Goroutines in Go use normal heap memory for stacks, and the compiler emits code to grow and move threads automatically. Rust proponents will tell you that async functions don't require any runtime cost because the stack requirements can be calculated statically. But to calculate this statically you can't support recursive functions. And if you can statically calculate your space requirements for the hidden async state object, you could also statically calculate the stack size for a thread just the same.
So really what it all comes down to isn't whether "async" is better or worse than "threads" along any of these dimensions. Abstractly, all threading implementations are async, and all async implementations effectively implement threads (i.e. a data structure that encapsulates a program counter, local automatic storage, etc). The real reason you choose one over the other is external factors. For Rust that dominate factor is interoperability with native C ABIs, particularly native stack disciplines. Because Rust can't implement much magic in the lower layers of the runtime environment while maintaining the degree of interoperability with C, C++, and other language libraries (via the C ABI) that they're committed to, they have no choice but to put most of the instrumentation into the language itself. And this necessitates the async contortions, independent of any other preferences. Contrast that with Go, where calling into C is slightly more costly because they preferred to push more of the async/thread abstraction beneath the language syntax.
But perhaps what this tells us is that we should think about revisiting native stack disciplines and thread scheduling semantics. IIRC, Linux will soon get scheduler activations (i.e. ability for userland to efficiently switch execution to another specified kernel-visible thread). That's a small step in the right direction, and if it catches on more operating systems will adopt this--after having ditched them 20 years ago, ironically, before async network I/O became popular and when 1:1 thread scheduling became the preferred kernel model).
"Reqwest" uses the Tokio machinery, even for a blocking request. If you turn on "Trace" level logging, you can watch it start up a thread pool and go through a 35-step process, using all the async and futures machinery, to do one synchronous request. Log messages include "handshake complete, spawning background dispatcher task" and "signaled close for runtime thread (ThreadId(2))"
This seems excessive.
I think this is the aspect of modern approaches async which I am most ambivalent about. One of the things I have learned about programming over the past 10 years is that I, as the programmer, really want to own the flow of control of my program. Once I hand that over to some other system, usually in the name of convenience, I will start to have issues which are difficult to understand and solve.
For instance, a while ago, I was working on a project which was using making heavy use of RXJava. One of my colleagues pushed a commit, and suddenly CI was failing on a unit test which passed when run locally. It turns out the problem was because the CI server was running tests with a different scheduler, so GC was happening at a different time, creating an NPE which didn't happen locally. Imo when you start to see unit tests behaving inconsistently based on a factor which is completely outside the actual code you yourself have written, this is a sign you are going down the wrong path.
I also wonder how much a lot of the buzz around async actually has to do with the fact that it's a bit brain-bending to wrap your head around at first, as compared to its actual utility. I think for a lot of us as programmers, we enjoy that feeling of understanding something difficult - like when you really get recursion for the first time - and we're attracted to the idea of really fundamentally new concepts being introduced to programming.
But it seems to me that async is one of those concepts which brings us farther away from actually programming the hardware, and puts a kind of middle-man between us and the CPU, and I'm not sure that has ever been a good thing.
I didn't know that cross "async" communication was cheaper, that does seem like a good selling point, but what exactly makes it cheaper? After all threads share the same address space, so you can just pass pointers around the same way you would within the same thread. I expected the overhead to be roughly similar.
Things can get cache-expensive if the code is running on different cores, but then again using all the hardware resources available is generally something you want to do if you care about performance.
Maybe one can enforce this convention in the particular project, but there's no ecosystem-wide consensus on this, and in fact I don't want this to be consensus. I write blocking non-async functions every day. Why am I wrong to do so?
What does 'blocking' mean? I would expect the definition of synchronous to be the exact opposite; i.e., a synchronous function must block the caller until the function has finished executing. For that matter, what is "regular logic"? The name implies there is some sort of "irregular logic" to contrast it with.
I get the feeling that the writing may be unclear because the concepts are themselves not well-defined.
That said, if you can get your job done without this stuff, that's fine too, but the reasons it was pursued specifically involve the above.
That being said I can't shake the feeling that going for something like Tokio in such a case is a bit like healing a paper cut by amputating the arm. Sure, technically you don't have the original problem anymore...
https://blog.rust-lang.org/2015/04/10/Fearless-Concurrency.h...
Web servers are all about I/O and handling small tasks (requests), and are a perfect use case for asynchronous programming.
> That sounds like premature optimization in many situations IMO
Maybe in some cases... but then just don't use futures. Rust does not have a runtime, so it gives you the choice. std is all blocking, so you can spawn threads and do event-driven programming, which might be just fine for a lot of people.
async is more ergonomic for Rust specific reasons, makes sense for a lot of use cases, and was a highly requested language feature, so it was added to the language. OS threads can work just fine for many people. If that includes you, you don't have to use async.
In principle yes, but in practice I disagree. Due to keep-alive you want to be able to handle many idle connections at the same time, but you rarely want to handle many active connections at the same time.
Example: Whenever you talk to another services (e.g. a database) you need to limit the number of connections you have open. You can't just blindly open a new connection per incoming request. This means that your practical level by parallelism is often bounded by your database. If every request talks to Postgres and you have a connection pool with a limit of 50, then there is nothing to gain by having support for 1000s of "active" connections. You'd rather want Postgres to focus on finishing existing requests than opening new ones.
And once you look into Postgres you'll observe the same thing: There's only limited amount of CPU/IO so there's no point in having 1000s of "active" requests going on at the same time.
The async concept has been used for decades in pretty much every product I've worked on professionally, from enterprise raid controllers to network protocol implementations and telephony software. An engineer I respect once told me that really it's the only way to write services at scale, and anything else is just a step on the road until you reinvent it. He was probably exaggerating, but it is very important, and nearly ubiquitous.
Having used custom frameworks for async code in C and C++, it's really refreshing to have it baked into the language and well supported. It's yet another arrow in Rust's fantastic quiver.
If you can't handle thousands of requests per second with thread per request, that's more about your software stack, not about threading.
I guess it was different in the past when computers were slow. I can believee that.
One is they make your functions colored; async functions world best with other async functions while normal blocking functions work best with other blocking functions.
They also introduce a lot of noise; putting async/await everywhere doesn't tell you anything interesting.
Considering a normal-sized Linux server can handle a million threads without much trouble, it really seems like misplaced effort.
I haven't used it yet, so I can only repeat the advertising copy, but nevertheless wanted to give some perspective from other ecosystems.
This is definitely a good thing. All computations should me "marked" as total or effectful with various possible effects (blocking, async, nondeterministic, possibly non-terminating etc etc).
Reasoning about your program is hard when each computation is a blackbox possibly containing any side-effects which could cause unpredictable changes in the control flow and result in a completely incomprehensible way.
Async/await thing is indeed least sound and ergonomic way of doing this. Monads with monad transformers are a bit better. Algebraic effects are the best in terms of composability, ergonomics and mental overhead, but not here yet (though OCaml may be soon become the first industrial-grade language incorporating them [1])
Not in Zig: https://kristoff.it/blog/zig-colorblind-async-await/
Seems like that would use a lot of memory for all the stacks
I don't get what's so bad about "colored" function. That color is just about the return type of the function.
How do you return an error from a function that does not return a Result? You must call unwrap (panic) or change the colour of your function by changing the return type to Result and fix all the caller.
Similarly, if you want to use a future from a non-async function, you either call `block_on(...)`, or you change the return type to a future by marking the function async.
I don't think it is that bad. That's just the way explicitly typed programming languages work.
Basically if you just ignore the async/await keyword, the code should read mostly the same as synchronous code (which is the point of the async/await effort).
Maybe you have a concrete example of convoluted async code?
Contrast this with the thread model, where the runtime needs to create and destroy threads where the code asks for it. In other words, your program logic is mixed up with runtime considerations.
For a practical example, let's say you want to use rust to extend some C code to create a network client that calls back into the C code. What if the C code is not thread-safe? With async, no problem, just use the CurrentThread runtime. This is my use case, anyway.
Why are there so many libraries that depend on Tokio then? If only the edges need to actually use an async runtime, the middle-tier libraries can just be plain futures.
I agree async has usability problems. Hopefully they'll get better over time; I'm looking forward to eventually having generators rather than dealing with futures::stream::StreamExt and the like.
I don't think everyone needs to use async all the time. Take a web app, for example. If the webserver is directly Internet-facing, it has to deal with lots of keepalive connections, so the core webserver logic (hyper or equivalent) should be async. But if you're not dealing with too many active requests and don't care about the performance difference, I don't think there's any reason you shouldn't have all your request handling just use threads, sending replies to hyper with a channel and blocking when necessary.
Last I checked I couldn't find an ergonomic and efficient "half-async" bounded channel implementation. By which I mean one that allows you to treat the sender as blocking and the receiver as async, or vice versa. That'd be really useful for writing synchronous programs that use async libraries. I certainly don't see any reason one couldn't exist. Maybe it already does and I missed it.
Today I'd maybe take a look into crossbeam and their queue implementations for this kind of synchronization.
No you’re not. There is a similar situation in Kotlin, which supports coroutines. Makes things more complicated and is often used for very questionable reasons.
This is why I’m excited about project Loom, which will use the same old thread abstraction but can be configured to use fibers under the hood instead. Java devs don’t even care that its not traditional threading, its the same API! This simultaneously solves the „coloredness“ problem of functions.
Why is it complicated? Because you didn't bother to learn new API and it is sufficiently different from older one?
I.e. parallelism means executing several tasks on different compute units (cores) at the same time (multiple threads abstraction). Concurrency simply means that you can execute several tasks over a period of time, but it can happen on the same compute unit (so even within one thread). Tasks could be interleaved and still all progress over time.
I guess some ideal usage is a combination of parallelism and concurrency, but using a separate thread for each task isn't necessarily the most optimal method, because threads have their gotchas like context switching and etc.
I've seen that a lot on the internet and I guess it must depend where you come from, because I find async/await orders of magnitude easier to reason about than threads+channel (and I'm not even talking about using epoll manually, which is just inscrutable as soon as there is a little bit of complexity involved)
My understanding from reading along with many of these conversations is that the ultimate goal of the async IO frameworks is to amortize system call costs across multiple IO operations, instead of having >1 per.
I've heard some speculation about system call overhead going up in order to guarantee correctness (for Spectre and Meltdown class scenarios, but also for some other classes of concurrency issues). In which case io_uring is the carrot and system call slowdown the stick.
For example, futures in Rust can be used with both OS threads and lightweight tasks. Tokio is mostly agnostic about the choice of the executor.
It definitely takes some time to grok async Rust (even if you come from C#/JS), but I think it really shines once you get to know it, similar to the benefits you get from learning about iterators & higher level functions as opposed to plain loops.
For instance, I've recently implemented the Raft protocol as part of a distributed algorithms course. Using Tokio and a single threaded executor made the implementation fairly readable, mostly relying on few async constructs(futures, tasks, channels and select loops) to model fairly complex behavior (bidirectional messaging, multiple states, timeouts, etc..) Doing so in a more traditional callback oriented style would've required maintaining a very complex state machine(in addition to the state machine of the algorithm itself)
Also, Async/Await originated in C#, a language which already supports threads(and many other concurrency models), not JavaScript which historically relied on callbacks
Isn't async by design more efficient even when you have multiple threads you can spawn? My understanding is the event loop would do async tasks in the thread's quiet times, and put those tasks to sleep while it's waiting for io and other things, meaning the thread isn't blocked. Comparing that to (my understanding of) threads, while you're not blocking the main thread, you're still blocking the spawned threads while waiting for io.
Isn't this thread blocking something you would want to avoid if you can regardless of whether or not you have additional threads to play with?
You gain some distinct advantages too by doing this, because you can then just run a single thread in your worker pool and if you ever need to make the program multi-threaded - god forbid - you can add locks to shared resources (or in Rust's case the compiler will help you with this) and then bump up the number of threads.
CPUs are really really fast today, I think engineers generally underestimate the amount of performance you can get with a single-thread and non-blocking I/O.
Async language constructs make this process a bit easier. The only issue IMO is that it is awkward to call async functions synchronously, or that it can have hidden costs to do so. I think languages will improve on this.
I believe on Linux, using non-blocking system calls on threads can help reduce expensive context switches too... whereas spawning multiple threads and having them use blocking system calls can cause more context switches.
I will say though.. I've seen developers just async-ify everything in codebases without thinking about why or if it is beneficial.
I don't know about the end of this sentence, but yeah, I'm the kind of person who enjoys very much writing async code and finds that it (sometimes) models the problem much better than sync code and much, much, much better than event-driven/polling programming.
But then, I'm also the kind of person who enjoys coding with CML channels (aka Rust mpsc channels aka Go channels aka Erlang mailboxes aka pi-calculus channels etc.), so I guess I might be a mutant.
The model doesn't work for all forms of concurrency, but I think it works for a lot of things that people at the "top of the stack" (application developers) do.
I don't know what kind of code you're looking at, in general, but you should be able to massage most async stuff into a list of things if you're not in callback soup. Granted, lots of people don't try to stay out of the callback soup, but.... I feel like even that is better than just like "try to validate concurrency invariants", which is a much harder problem for arbitrary code IMO?
Besides that it provides lots of utilities for working with async code.
Oh man. C++'s std::future has the same silliness. I've come to the conclusion that futures/promises are a dumb abstraction.
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.)
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.
"""smol (likely discontinued, since author left rust)""" doesn't seem well supported and smol is used by async-rs these days iirc.
[0]: https://github.com/async-rs/async-std
Most libraries can be used with different runtimes. Hyper for example, which uses Tokio by default, can be configured to use an async-std executor.
I'd be very interested in hearing other opinions, as my Rust project [1] is currently stuck on an older version of Tokio while I wait for deps to update. I'm either going to have to bite the bullet and replace deps or bite more bullets and find a different runtime.
As I understand it, that difference (vs Tokio) was the main driver behind the projects splitting.
I also think it's a bit presumptuous for them to name themselves "std". It'll be even more ridiculous in the likely event that Tokio becomes the std:: asynchronous I/O library. It's asking for confusion.
do you have an example?
Either way, "can be configured" means that custom code for each runtime must be written, it is not like lets say "Futures", which can be used generically.
Whoa. smol is great. Why’d the author leave?
If you're writing a library to be used by others you can very often expose only types which come from std::futures. The result will work with all of the runtimes.
Using runtime-specific features and APIs ties you to a specific runtime.
Both epoll and io_uring have virtually the same performance. Of course uring is a lot more "ergonomic" that's why it already has amazing momentum.
It should also, at some point, allow for nice zero copy network receive paths under the right circumstances (i.e. the network card DMAing directly into the userspace buffers, without very weird setup/high op overhead).
I tested rio recently as I had a Brilliant but Bad Idea™ involving file access and was pleasantly surprised by the API, as I have been with sled's.
I'm excited for the experimentation in the Rust ecosystem and for such low level crates to handle the complex io_uring tasks (relatively) safely!
I'd honestly like to know too.
One problem with this is that an api like tokio’s might appear to be asynchronous but in reality portions of it are “just” synchronous API calls marshaled to a thread pool - i.e. none of the real benefits of async for now, but positioned so that the library can be switched over to real async code and automatically take all the consumers with “it in the future.”
I’m glad to hear mention of io_uring because it means that IOCP on Windows might get some love. For those that don’t know, on Linux there is^H^H was really no such thing as properly async file system access (eg libc faked it in a similar fashion for aio) so libraries like mio didn’t bother with true async for non-network parts of the library (and also partially because the biggest motivation for async development was the web world which doesn’t particularly care about asynchronously listing the contents of a directory or writing a “highest” performance backup product) - even though at least some platforms (like Windows) had very compelling async options available across the board.
That's not quite right - there didn't use to be AIO for buffered filesystem IO and for most operations beyond read/write.
But unbuffered reads/writes have been doable asynchronously for quite a long time, via io_submit/libaio. Without falling back to threads.
The restrictions around that can be onerous (e.g. one needs to be careful to not extend file sizes, or risk falling back to synchronous operation).
That is not quite right because it is dependent on the filesystem. This is why scylla database requires XFS, because they rely on actual async file io using io_submit.
Did they change libaio to work without O_DIRECT at some point? Or are you talking about io_uring for async file io?
That only happens when OS support is missing. As in, the OS does not support doing something asynchronously.
Once io_uring is implemented, there will be no more of these "portions" (at least not on Linux).
If anyone's got minimal code lining up a web framework (any one, not stuck to actix) with some reqwest, I'd be thankful to look over it. Just some trivial stuff so I can add an API gateway that proxies a specific API.
todos
}
```Obviously this code won't run, just want to gleam the gist of what you want from an example.
If it could do the Error case too that would be cool.
For Rocket, I have something like:
#[get("/list/<status>")]
fn list_prospects(api_key: ApiKey, status: String) -> Result<Json<PendingList>, Status>
with a impl<'a, 'r> FromRequest<'a, 'r> for ApiKey
But I couldn't quickly get an `async` piece in there so I just sucked it up and synced it all up since it's only backing a Retool dashboard so it isn't the end of the world.So if the example is like:
#[get('/todo')]
fn get_todos() -> Result<Json<TodoList>, Status>
let todos = reqwest...
Ok(todos)
or the equivalent in the web framework you have that would be hecka useful.Also, I love the fact that Rust will complain if the examples in your comments don't compile. Such a great feature. As a result, copy-and-pasting examples out of rustdoc pages (nearly) always gives you a working starting point to hack from.
Basically, just like C# (and VB.NET and C++ .NET task/then) provides syntax and semantics for async-await, Rust provides it at language level too. (And it defines how the compiler transforms it into Future objects.)
But, since Rust doesn't have a mandatory runtime, something needs to implement the low-level stuff that knows what to do with these Future objects. (In Tokio you have a work-stealing threadpool, but maybe in smaller runtimes you don't need all that fancy stuff for high-throughput, you just need small binary size, so there's a runtime/library called "smol" that's main feature is that it's a small async runtime.) In the CLR as far as I know there are Task objects, which basically correspond to Rust's Future objects.
One interesting low-level difference (similarity?) is that in the CLR there's an explicit callback support by the runtime (to wake up Task objects - which can lead to deadlocks if they are scheduled on the UI thread), whereas in Rust Futures pass their own callbacks (called Waker) to a thing called the Reactor (which is basically the low-level implementation of the Executor, which binds to the OS/kernel level primitives, such as epoll or IOCP).
And even though it's a "zero cost" abstraction, it still means there's a state machine, just like in .NET. Except it's built and "deadlock checked" at compile time.
https://tooslowexception.com/wp-content/uploads/2020/05/ther...
https://www.red-gate.com/simple-talk/dotnet/net-framework/th...
My specific example is writing a fuse handler (now with cberner/fuser formerly zargony/rust-fuse) for GCS/S3. If you want to use make any async requests (like via hyper), you currently have to roll your own poller, like reqwest does in blocking mode [1].
The rust async/.await primer [2] offers the reader the seemingly helpful futures::executor::block_on, but basically no two executors can interact (and for good reason!). As others highlight, the ecosystem seems like it's going to end up standardizing on tokio (and/or some flavor thereof) and that hopefully now that it's 1.0, we can have stable enough deps for a while :).
[1] https://github.com/seanmonstar/reqwest/blob/5ee4fe5ab64a2e3d...
[2] https://rust-lang.github.io/async-book/01_getting_started/04...
https://docs.rs/futures-lite/1.11.3/futures_lite/future/fn.b...
This feels like a slap in the face for some reason.
Aaron Turon is an extremely talented individual (their PhD thesis was a landmark contributionn). They are super kind and one of the nicest human beings I've ever met. They led the Rust Project until their involvement with Tokio caused them to drop off from Tech.
Alex Crichton is an extremely talented, kind, and hyperproductive individual, who after their involvement with Tokio dropped all async/await work and luckily "refocused" on WebAssembly.
If one is going to recap the road towards Tokio 1.0 and mention all the people that have left the Rust async ecosystem or Rust all together, you might as well spell things out.
Are you sure this isn't something else instead?
$ cargo tree -e no-dev,no-build --no-dedupe -p tokio
tokio v1.0.0
├── bytes v1.0.0
├── libc v0.2.81
├── memchr v2.3.4
├── mio v0.7.6
│ ├── libc v0.2.81
│ └── log v0.4.11
│ └── cfg-if v0.1.10
├── num_cpus v1.13.0
│ └── libc v0.2.81
├── once_cell v1.5.2
├── parking_lot v0.11.1
│ ├── instant v0.1.9
│ │ └── cfg-if v1.0.0
│ ├── lock_api v0.4.2
│ │ └── scopeguard v1.1.0
│ └── parking_lot_core v0.8.2
│ ├── cfg-if v1.0.0
│ ├── instant v0.1.9 (*)
│ ├── libc v0.2.81
│ └── smallvec v1.4.2
├── pin-project-lite v0.2.0
└── signal-hook-registry v1.3.0
└── libc v0.2.81
There are a lot more dependencies that are used only for the build scripts (build-dependencies) or for running the tests, examples, and benchmarks (dev-dependencies). None of those dependencies cause any additional code to wind up in your binaries when your project depends on tokio.PS, I think there's a bug in "cargo tree"... the command above actually prints out only one line ("pin-project"). I had to remove the "-p tokio" and then copy-and-paste out the relevant section.
"we are committing to providing a stable foundation..."
I am curious: Who is "we"? I have no priors, I really have no idea.
Perhaps it is not answerable but it is a important question, when guarantees are made, who is making them?
I am not sure it is a important question in that it is not a important guarantee. The software is what it is, it is open and modifiable. But if the guarantee mattered then this would be a crucial question and the answer would describe the organisational structure of the "tokio maintainers and contributors" as a group
That's part of its awesomeness. That's why it can target microcontrollers.
You're probably used to languages with garbage collectors. Having garbage collection forces you to have a "runtime" since that's where the GC code goes. Then more and more stuff accretes onto this unavoidable runtime, and before you know it you're writing Java code...
Anything that has a language keyword should have a default implementation in that runtime, and much like box, ?, !, etc async and await are both language features that I shouldn't need to include from outside of the runtime.
Thanks again! Looking forward to all the good things still in the pipeline!
Congrats to the tokio team + contributors. <:)
I'm excited for GATs to land so we can have true async trait methods. The async story in Rust has come a long way, but there's still a lot to be improved.
As a new rustacean it was really difficult to try and use the the async/await syntax with horde of adapters required for tokio 0.1/0.2. I ended up trying out async-std too, but the move to smol had its own issues and I lost interest.
A 1.0 release with stability commitments is exactly what I need to get back into experimenting with Rust.
Crate consumers have to consider which async lib a crate is using leads to a lot of annoying gotchas that can really confuse less experienced rust devs.
I'm hoping tokio becomes the default and eventually gets merged into std, it really is the best async implementation.