This is incorrect, so all of the philosophical comments that follow (which in abstract I agree with) are not very relevant. It's really frustrating when our tools work in ways we don't understand, so I empathize! But there's no special treatment or any such thing going on in relation to Send and Sync with async/await. I'll try to explain:
A hint at the mistake in your understanding is when you write "send references across .await
boundaries" - there is no such thing as "sending something across an await boundary." An await boundary represents a point at which a future can pause and yield control, nothing is sent across the boundary.
A multithreaded executor may send that future, while it is paused, to another thread, and resume polling it on that new thread. Hence, when a spawn API requires that a future implement Send
, it requires that all of its state is Send
. This is true universally - if any type held across an await point is not Send
, the future will not be Send
. There is no magic here.
But there are also several types in std which are Send
but not Sync
- and if a type is not Sync
, shared references to it are not Send
. Therefore it is perfectly allowed to hold these types by value across await points, but not allowed to hold references to them across await points. That distinction is probably the root of what seems like inconsistent behavior.
My recommendation is just not to use types that don't implement both Send and Sync in async functions, period. Then refactors can never cause these kinds of errors.