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.
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.