It's certainly possible to pave over the difference between models to a certain extent, but the resulting solution will not be zero-cost.
Yes, there is a fundamental difference between those models (otherwise we would not have two separate models).
In a poll-based model interactions between task and runtime look roughly like this:
- task to runtime: I want to read data on this file descriptor.
- runtime: FD is ready, I'll wake-up the task.
- task: great, FD is ready! I will read data from FD and then will process it.
While in a completion based model it looks roughly like this:
- task to runtime: I want data to be read into this buffer which is part of my state.
- runtime: the requested buffer is filled, I'll wake-up the task.
- task: great requested data is in the buffer! I can process it.
As you can see the primary difference is that in the latter model the buffer becomes "owned" by runtime/OS while task is suspended. It means that you can not simply drop a task if you no longer need its results, like Rust currently assumes. You have either wait for the data read request to complete or to (possibly asynchronously) request cancellation of this request. With the current Rust async if you want to integrate with io-uring you would have to use awkward buffers managed by runtime, instead of simple buffers which are part of the task state.
Even outside of integration with io-uring/IOCP we have use-cases which require async Drop and we currently don't have a good solution for it. So I don't think that the decision to allow dropping tasks without an explicit cancellation was a good one, even despite the convenience which it brings.
Yes, there is a fundamental difference between those models (otherwise we would not have two separate models).
In a poll-based model interactions between task and runtime look roughly like this:
- task to runtime: I want to read data on this file descriptor.
- runtime: FD is ready, I'll wake-up the task.
- task: great, FD is ready! I will read data from FD and then will process it.
While in a completion based model it looks roughly like this:
- task to runtime: I want data to be read into this buffer which is part of my state.
- runtime: the requested buffer is filled, I'll wake-up the task.
- task: great requested data is in the buffer! I can process it.
As you can see the primary difference is that in the latter model the buffer becomes "owned" by runtime/OS while task is suspended. It means that you can not simply drop a task if you no longer need its results, like Rust currently assumes. You have either wait for the data read request to complete or to (possibly asynchronously) request cancellation of this request. With the current Rust async if you want to integrate with io-uring you would have to use awkward buffers managed by runtime, instead of simple buffers which are part of the task state.
Even outside of integration with io-uring/IOCP we have use-cases which require async Drop and we currently don't have a good solution for it. So I don't think that the decision to allow dropping tasks without an explicit cancellation was a good one, even despite the convenience which it brings.