I think there are multiple definitions out there for what exactly the differences between the Reactor and Proactor design patterns are. The way I currently think about it, it's best to focus on the semantics of repeated reads for this.
Reactor: A user registers a channel with the reactor, providing a callback. After this, whenever there is new data available to be read, the reactor notifies the user of this by calling this callback. The user can then read some data from the channel in the callback. So providing a single callback (at registration time) can lead to multiple invocations of this callback.
Proactor: Whenever a user wants to read some data from a channel, they make an asynchronous request to the proactor, providing a callback and a pointer/size to some buffer-space. After this, when new data is available to be read in the channel, the proactor copies this data into the user provided buffer-space and calls the callback. This completes the asynchronous request. The user has to make sure that the buffer-space remains valid/in-scope/writable until the asynchronous request completes. If the user wants to read more data, they have to explicitly make another asynchronous request. So providing a single callback (at request time) always leads to exactly one invocation of the callback. In this pattern, callbacks are usually written so that at the end of the callback another asynchronous request is made (this is called "chain of asynchronous operations" in the ASIO documentation; ASIO follows the Proactor pattern).
The difference can be exemplified by the Linux APIs epoll (=reactor) and io_uring (=proactor). With epoll, you do ::epoll_ctl(fd, EPOLL_CTL_ADD, ...) to add a file-descriptor to the kernel epoll object once, and the file-descriptor can then appear multiple times in the result from ::epoll_wait(...). With io_uring users receive one completion-queue-entry (cqe) for every submission-queue-entry (sqe) they've made (but I'm not too familiar with io_uring; I might be wrong here).
So the proactor pattern can cause more overhead when there are a lot of short reads. I suspect that this is the reason why io_uring can sometimes be slower than epoll:
It's possible to give different definitions, though. For example, the definition could revolve around whether (a.1) the user provides a pointer/size to some buffer-space that they guarantee to be valid until the asynchronous request is finished or whether (a.2) the user proactively reads the data into their buffer inside the callback. One could also focus the definition around whether (b.1) callbacks are used (in which the user can react to events) or whether (b.2) the user proactively checks some data-structure for which channels are readable. These aspects are orthogonal to the the aspect described above, though, and I've found it most helpful to center the definition around whether (c.1) user callbacks usually have to proactively issue a new asynchronous request or not (c.2).
- ASIO is (a.1) (b.1) (c.1)
- epoll is (a.2) (b.2) (c.2)
- io_uring is (a.1) (b.2) (c.1) (I think, but I might be wrong)