You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Context and Waker might be accidentally Sync #66481
One issue that came up in the discussion here is that the Context type implements Send + Sync.
This might have been introduced accidentally. Send probably does not matter at all, given that users will only observe a Context by reference. However Sync has the impliciation that we will not be able to add non thread-safe methods to Context - e.g. in order to optimize thread-local wake-ups again.
It might be interesting to see whether Send and Sync support could be removed from the type. Unfortunately that is however a breaking change - even though it is not likely that any code currently uses Context out of the direct poll() path.
In a similar fashion it had been observed in the implementation of #65875 (comment) that the Waker type is also Send and Sync. While it had been expected for Send- given that Wakers are used to wake-up tasks from different threads, it might not have been for Sync. One downside of Wakers being Sync is that it prevents optimizations. E.g. in linked ticket the original implementation contained an optimization that while a Waker was not cloned (and thereby equipped with a different vtable) it could wake-up the local eventloop again by just setting a non-synchronized boolean flag in the current thread. However given that &Waker could be transferred to another thread, and .wake_by_ref() called from there even within the .poll() context - this optmization is invalid.
Here it would also be interesting if the Sync requirement could be removed. I expect the amount of real-world usages to be in the same ballpark as sending &Context across threads - hopefully 0.
But it's again a breaking change 😢
Even if this weren't a breaking change, I'd want to see some really good evidence these optimizations would matter for real world code before choosing to make these types non-Sync. I don't think getting rid of an atomic update in thread::block_on is a compelling example. Basically, in my view the current API is intentional and correct.
Context was introduced to leave room for future unforeseen requirements, and being Sync restricts its usefulness as such, which is unfortunate. I can see a case for Waker: Sync since we have wake_by_ref, but it's difficult to imagine a case where it makes sense to be accessing a single Context from multiple threads concurrently.
I have an example of where the current API is resulting in performance issues: I am trying to implement a combined Executor/Reactor around a winit event loop. To wake up the event loop from a different thread we have to post a message to the event loop which can be a non-trivial operation. But on the same thread, we know that we don't need to wake up the eventloop, so we can use much cheaper mechanisms like an atomic bool.
I think the point is that Context being Send + Sync precludes the otherwise noninvasive restoration of a more efficient LocalWaker through an additional method on Context.
Even if this weren't a breaking change, I'd want to see some really good evidence these optimizations would matter for real world code before choosing to make these types non-Sync. I don't think getting rid of an atomic update in thread::block_on is a compelling example.
I don't think it should be the judgement of the libs team (or any of us) to determine whether something is good enough and not needs to be further optimized for any use-case. Rusts goal as a language is to enable zero cost abstractions. This requires in my opinion to not be opinionated on any design choices which have a performance/footprint impact. I think some of the decisions that have been taken in the async/await world however are opinionated, and will have an impact on use-cases that currently are still better implemented with different mechanisms.
I do not really want to elaborate on anything more particular, because I fear this would bring the discussion back to a arguing whether any X% performance degradation is still good enough. That is an OK decision to have for a software project whichs needs to decide whether Rusts async/await support is good enough for them, and whether they have to apply workarounds or not. But it's not a discussion which will move the issue here any more forward, because for yet another project the outcoming of the discussion might be different.
PS: I do think it's okay and good if libraries like tokio or async-std are being more opinionated about what threading models they support, and they might sacrifice performance gains in rare usage scenarios for easy of use. But core and language features are different, and we expect people to use them also for niche use-cases (e.g. there is certainly a lot of cool evaluation going on with the use of async/await in embedded contexts or kernels - where requirements might be very different than for a webserver based on tokio).
Reacted by Karl Bergström, Benjamin Saunders, worktycho, Harry Barber, protty, Didrik Nordström, Joshua Yanovski, Ibraheem Ahmed, Ryan-Git, tvallotton and 8 more
I don't think it should be the judgement of the libs team (or any of us) to determine whether something is good enough and not needs to be further optimized for any use-case. Rusts goal as a language is to enable zero cost abstractions.
This is a trade off: Context is either Sync or it isn't, some users benefit from one choice (they can use references to Context as threadsafe) and some users benefit from the other choice (they can have nonthreadsafe constructs inside the Context construct). Ultimately the libs team has to decide one way or the other on these points in the API where there is a trade off between thread safety and not.
However, this decision has already been made and it would be a breaking change to change it, so this discussion is moot.
(they can use references to Context as threadsafe)
Do you have an example where this is actually done within the ecosystem, or a use-case for it? As far as I can tell, this would require spawning a thread using something like crossbeam_utils::thread::scope and capturing the Context from within a Future::poll?
As @kabergstrom mentioned, the most likely way to see this behavior is some scoped thread API being used inside poll - or people doing some very advanced synchronization against an IO driver running in another thread. For all those there are certainly better ways than to rely on Waker being Sync. E.g. to return a flag from the synchronized section and call waker.wake_by_ref() from the original poll() thread when done. Or to .clone() and send the Waker if there is doubt whether it needs to be persisted somewhere else.
Why would these be "certainly better" than just calling wake_by_ref from the scoped threads?
To be a little more expansive: the scoped threadpool you're talking about could very well not be scoped inside a poll method - rather, the waker could be cloned once and then owned by one thread and referenced by many other scoped threads in some sort of threadpool construct for CPU bound tasks. This seems like a perfectly valid implementation which allows you to divide up the work among many threads without cloning the waker many times. This is potentially an optimization.
I don't think this optimization is very important, but I don't think the optimizations allowed by making waker Send + !Sync are very important either. The point is that there's a trade off between the optimizations allowed by assuming references to wakers can be shared across threads and the optimizations allowed by assuming they can't be, its not the case that one side is inherently the "zero cost" side.
Though I don't have the bandwidth to carry this, this definitely seems interesting. Seeing projects such as glommio and closed-source initiatives lead me to believe that there may actually be a decent case to enable some form of Context -> LocalWaker to avoid the synchronization overhead on single-threaded runtimes.
I don't know if we should make this a priority, but at least we probably shouldn't close it just yet.
I think bringing LocalWaker back would be a nice additional improvement. But for the moment it would be nice to just fix the general Context sync-ness, which blocks all other fixes and improvements.
Reacted by Benjamin Saunders, hadroncfy, Joshua Yanovski, Dennis Krasnov, Michael Velbaum and Eric Dunaway
As the person who originally introduced LocalWaker, it was very much a conscious decision to get rid of it and to make Waker and ContextSync. This was called out explicitly in the RFC.
The cited discussion in the RFC justifies making WakerSend, and predates the decision to (re)introduce Context. Making Context!Send + !Sync doesn't defeat the objectives discussed there. In particular, it does not impact ergonomics for the common case at all.
Reacted by Alex Kladov, Yosh, Joshua Yanovski, Dennis Krasnov, AlphaModder, Diggory Hardy, Andy Wright and Eric Dunaway
Waker supports wake_by_ref, and so it is possible to pass &Waker to another thread and wake it from that thread. This functionality would not be possible without Waker being Sync.
Supporting wakers that are either not Send or not Sync will best be done by adding new APIs to Context and a new LocalWaker type.
Forgive me the naive question, but how does a LocalWaker type work with Future? As I understand, Future::poll requires Waker, as a concrete type not a trait, so this would also require a LocalFuture trait? At this point we have two completely incompatible async systems...
... which means it's very likely not going to happen. I thought as much.
so it is possible to pass &Waker to another thread and wake it from that thread
Which is only useful if Waker is not cheap to clone, and has the burden of another lifetime bound. It surprises me that the Waker docs don't say anything about the intended cost of Waker::clone.
Forgive me the naive question, but how does a LocalWaker type work with Future? As I understand, Future::poll requires Waker, as a concrete type not a trait, so this would also require a LocalFuture trait? At this point we have two completely incompatible async systems...
Future::poll does not take Waker as an argument, Future::poll takes Context. Context could have the ability to set a LocalWaker, so that an executor could set this. Reactors which operate on the same thread as the future that polls them could migrate to using the LocalWaker argument instead of Waker. Here is a pre-RFC that someone wrote with a possible API: https://internals.rust-lang.org/t/pre-rfc-local-wakers/17962
... which means it's very likely not going to happen. I thought as much.
Commentary like this is both factually wrong and not helpful for the mood of the thread.
Activity
withoutboats commentedon Nov 17, 2019
Even if this weren't a breaking change, I'd want to see some really good evidence these optimizations would matter for real world code before choosing to make these types non-
Sync. I don't think getting rid of an atomic update inthread::block_onis a compelling example. Basically, in my view the current API is intentional and correct.Ralith commentedon Nov 17, 2019
Contextwas introduced to leave room for future unforeseen requirements, and beingSyncrestricts its usefulness as such, which is unfortunate. I can see a case forWaker: Syncsince we havewake_by_ref, but it's difficult to imagine a case where it makes sense to be accessing a singleContextfrom multiple threads concurrently.worktycho commentedon Nov 17, 2019
I have an example of where the current API is resulting in performance issues: I am trying to implement a combined Executor/Reactor around a winit event loop. To wake up the event loop from a different thread we have to post a message to the event loop which can be a non-trivial operation. But on the same thread, we know that we don't need to wake up the eventloop, so we can use much cheaper mechanisms like an atomic bool.
withoutboats commentedon Nov 17, 2019
@worktycho But that would be true if wakers were just
Sendalso, which was an intentional part of the simplification from the previous design.Ralith commentedon Nov 17, 2019
I think the point is that
ContextbeingSend + Syncprecludes the otherwise noninvasive restoration of a more efficientLocalWakerthrough an additional method onContext.Matthias247 commentedon Dec 1, 2019
I don't think it should be the judgement of the libs team (or any of us) to determine whether something is good enough and not needs to be further optimized for any use-case. Rusts goal as a language is to enable zero cost abstractions. This requires in my opinion to not be opinionated on any design choices which have a performance/footprint impact. I think some of the decisions that have been taken in the async/await world however are opinionated, and will have an impact on use-cases that currently are still better implemented with different mechanisms.
I do not really want to elaborate on anything more particular, because I fear this would bring the discussion back to a arguing whether any X% performance degradation is still good enough. That is an OK decision to have for a software project whichs needs to decide whether Rusts async/await support is good enough for them, and whether they have to apply workarounds or not. But it's not a discussion which will move the issue here any more forward, because for yet another project the outcoming of the discussion might be different.
PS: I do think it's okay and good if libraries like tokio or async-std are being more opinionated about what threading models they support, and they might sacrifice performance gains in rare usage scenarios for easy of use. But
coreand language features are different, and we expect people to use them also for niche use-cases (e.g. there is certainly a lot of cool evaluation going on with the use of async/await in embedded contexts or kernels - where requirements might be very different than for a webserver based on tokio).withoutboats commentedon Dec 3, 2019
This is a trade off: Context is either Sync or it isn't, some users benefit from one choice (they can use references to Context as threadsafe) and some users benefit from the other choice (they can have nonthreadsafe constructs inside the Context construct). Ultimately the libs team has to decide one way or the other on these points in the API where there is a trade off between thread safety and not.
However, this decision has already been made and it would be a breaking change to change it, so this discussion is moot.
kabergstrom commentedon Dec 3, 2019
Do you have an example where this is actually done within the ecosystem, or a use-case for it? As far as I can tell, this would require spawning a thread using something like
crossbeam_utils::thread::scopeand capturing theContextfrom within aFuture::poll?3 remaining items
withoutboats commentedon Dec 10, 2019
Why would these be "certainly better" than just calling
wake_by_reffrom the scoped threads?withoutboats commentedon Dec 10, 2019
To be a little more expansive: the scoped threadpool you're talking about could very well not be scoped inside a poll method - rather, the waker could be cloned once and then owned by one thread and referenced by many other scoped threads in some sort of threadpool construct for CPU bound tasks. This seems like a perfectly valid implementation which allows you to divide up the work among many threads without cloning the waker many times. This is potentially an optimization.
I don't think this optimization is very important, but I don't think the optimizations allowed by making waker
Send + !Syncare very important either. The point is that there's a trade off between the optimizations allowed by assuming references to wakers can be shared across threads and the optimizations allowed by assuming they can't be, its not the case that one side is inherently the "zero cost" side.KodrAus commentedon Jan 27, 2021
We discussed this at the recent Libs meeting and felt that deferring to @rust-lang/wg-async-foundations would make sense here.
yoshuawuyts commentedon Feb 16, 2021
Though I don't have the bandwidth to carry this, this definitely seems interesting. Seeing projects such as
glommioand closed-source initiatives lead me to believe that there may actually be a decent case to enable some form ofContext->LocalWakerto avoid the synchronization overhead on single-threaded runtimes.I don't know if we should make this a priority, but at least we probably shouldn't close it just yet.
Matthias247 commentedon Feb 16, 2021
I think bringing
LocalWakerback would be a nice additional improvement. But for the moment it would be nice to just fix the generalContextsync-ness, which blocks all other fixes and improvements.cramertj commentedon Mar 2, 2021
As the person who originally introduced
LocalWaker, it was very much a conscious decision to get rid of it and to makeWakerandContextSync. This was called out explicitly in the RFC.Ralith commentedon Mar 2, 2021
The cited discussion in the RFC justifies making
WakerSend, and predates the decision to (re)introduceContext. MakingContext!Send + !Syncdoesn't defeat the objectives discussed there. In particular, it does not impact ergonomics for the common case at all.Rollup merge of rust-lang#95985 - jihiggins:issue-66481, r=dtolnay
dhardy commentedon Jan 11, 2023
This was recently closed, however the issue mentions both
ContextandWaker. The recent change only affectsContext.There is good reason that
WakerisSend, however after reading this issue I'm unsure that there are good reasons whyWakerisSync.withoutboats commentedon Jan 12, 2023
(NOT A CONTRIBUTION)
Waker supports wake_by_ref, and so it is possible to pass
&Wakerto another thread and wake it from that thread. This functionality would not be possible without Waker being Sync.Supporting wakers that are either not Send or not Sync will best be done by adding new APIs to Context and a new LocalWaker type.
dhardy commentedon Jan 12, 2023
Forgive me the naive question, but how does a
LocalWakertype work withFuture? As I understand,Future::pollrequiresWaker, as a concrete type not a trait, so this would also require aLocalFuturetrait? At this point we have two completely incompatible async systems...... which means it's very likely not going to happen. I thought as much.Which is only useful if
Wakeris not cheap to clone, and has the burden of another lifetime bound. It surprises me that theWakerdocs don't say anything about the intended cost ofWaker::clone.withoutboats commentedon Jan 12, 2023
(NOT A CONTRIBUTION)
Future::poll does not take Waker as an argument, Future::poll takes Context. Context could have the ability to set a LocalWaker, so that an executor could set this. Reactors which operate on the same thread as the future that polls them could migrate to using the LocalWaker argument instead of Waker. Here is a pre-RFC that someone wrote with a possible API: https://internals.rust-lang.org/t/pre-rfc-local-wakers/17962
Commentary like this is both factually wrong and not helpful for the mood of the thread.