Edit: Hmm... The more I think about it the more I realize I probably have some very fundamental misunderstandings of what tasks are.
I'm working on a project that requires me to hash some files of arbitrary size, and I decided to try to do it using a custom file hasher future. See Looking for threaded/async tips for a specific problem (handing over writer socket) - #4 by blonk
In that thread @alice showed how easily one can make sure custom futures are executed (simply instantiate and call await in the context of an executor) -- but I want to be able to run more than one hash future in parallel. Thinking about this made me realize I'm not quite sure how these things work, and I think I may need some clarifications.
I use libevent quite a bit in C/C++, and while I realize async/futures aren't really a 1:1 equivalent to what libevent does, there are some important similarities, and I'd like to use what I know about libevent to see if I understand async/futures correctly. I'm hoping that this post, and any potential corrections to it, can be helpful to others coming from libevent/libev to rust.
With libevent one creates a base object, and within it one creates event objects. The event objects have callback functions that are registered to specific events, like "read" , "write" and "other". Once the OS detects an event the application has requested to handle it calls the appropriate event handler function.
The way I see it the async/futures framework in Rust is a generalization of what libevent does, but it isn't limited to the things libevent supports, and it's also not an isolated library like libevent is. But am I correct in assuming that:
An "executor" is more or less the dispatch loop in libevent.
A "future" (implementation) is a system which allows arbitrary events to be waited on. libevent is hard coded to support timers, kqueue, /dev/epoll, et al, i.e. it will always be limited to those events. A custom future can support those subsystems but can in addition do anything else (it could for instance be used to act on raw GPIO input in an embedded system).
A "future" (instantiation) is much like event contexts/handlers in libevent, but with the important difference that futures allows the language to create a state machine that allows waits to occur at any place in the async context while allowing the dispatcher thread to return to the main dispatch loop and process another future, and later resume the future where it was.
And this is where it gets a little confusing to me: What is a "task"? Are tasks what are actually "executed" by the executors? So let's say in my case that I want to be able to hash four files in parallel, does this mean that I have to instantiate four different futures and place them within four separate tasks and allow the executor to run?
In a sense, should one see "tasks" as the actual libevent event callback registrations?
I think much of my confusion could be cleared up if someone could tell me if this is accurate:
If a future A creates a new future B as soon as A is completed (within the same task), then we've created a sequential flow which transitions from A to B. But if A and B are created at the same time and placed within two different tasks, then the two will be run in parallel (well, in the order the executor decides to run them, but assuming both futures launch long-running threads).
And in my case, if I have multiple hashers running simultaneously -- how do I get their individual return values? The HasherFuture returns a struct that contains the file's hash, but if there are four of them running at the same time, how do I get the next one to complete without blocking the others? I.e. I don't want to wait for a specific one to complete, I want to wait until any of them finishes (the one that completes first), and then I want to extract its return value to I can send the result along to another process. Is this something that the particular executor one uses has to support?
I realized I really don't have a good grasp on how these things work when I realized that I can't simply do something akin to:
let result = collection_of_futures.wait_for_any();
println!("hash: {}", result.hash);
... because any future could be running, and there's no way to know how the type of the result variable.