Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Defer change callbacks to a worker thread #212

Merged
merged 10 commits into from
Jan 5, 2025
Merged

Defer change callbacks to a worker thread #212

merged 10 commits into from
Jan 5, 2025

Conversation

ecton
Copy link
Member

@ecton ecton commented Nov 18, 2024

This change fundamentally changes how change callbacks work on Dynamics. Prior to this change, callbacks executed on the thread that was performing the change. This could lead to situations where multiple threads were executing callback chains which leads to unpredictable locking patterns on the dynamics. The basic deadlock detection was not enough.

This change defers callbacks to a single callback thread. This thread ensures that no dynamic can have callbacks enqueued more than once. By limiting execution to one set of callbacks at any given time, this greatly reduces the surface for locks to contend with each other.

The next issue was how tuple-related functions like for_each/map_each were acquiring their locks. By calling a.read() then b.read(), this was causing a to be held in a locked state while b was being aquired. If users are careful to always acquire their locks in this order, everything is fine. But with Cushy there can be unexpected situations where these locks are being held.

This change also refactors lock acquisition for tuples to try to acquire all the locks in a non-blocking way. If any lock woould block, the initial locks are dropped while the lock that would block is waited on. After this is acquired the process starts over again to gain all the locks. This isn't perfect, but it doesn't require unsafe. With unsafe, we could in theory create a ring of callbacks that handles acquiring all of the locks into MaybeUninits. Upon successfully calling all callbacks, the values can be assumed init. But writing all of this in macro_rules isn't fun, and the current solution alleviates the main problem

This change fundamentally changes how change callbacks work on Dynamics.
Prior to this change, callbacks executed on the thread that was
performing the change. This could lead to situations where multiple
threads were executing callback chains which leads to unpredictable
locking patterns on the dynamics. The basic deadlock detection was not
enough.

This change defers callbacks to a single callback thread. This thread
ensures that no dynamic can have callbacks enqueued more than once.
By limiting execution to one set of callbacks at any given time, this
greatly reduces the surface for locks to contend with each other.

The next issue was how tuple-related functions like for_each/map_each
were acquiring their locks. By calling a.read() then b.read(), this was
causing a to be held in a locked state while b was being aquired. If
users are careful to always acquire their locks in this order,
everything is fine. But with Cushy there can be unexpected situations
where these locks are being held.

This change also refactors lock acquisition for tuples to try to acquire
all the locks in a non-blocking way. If any lock woould block, the initial
locks are dropped while the lock that would block is waited on. After
this is acquired the process starts over again to gain all the locks.
This isn't perfect, but it doesn't require unsafe. With unsafe, we could
in theory create a ring of callbacks that handles acquiring all of the
locks into MaybeUninits. Upon successfully calling all callbacks, the
values can be assumed init. But writing all of this in macro_rules isn't
fun, and the current solution alleviates the main problem
Currently only at the trace level, this diagnostic can be helpeful in
identifying if there are any callback cycles that are causing repeated
firings.
@ecton ecton marked this pull request as ready for review January 5, 2025 00:04
ecton added 7 commits January 5, 2025 06:50
At some point the old steps started failing, this seems to be the new
set of packages the wgpu project is using.
One of the fallouts of executing callbacks in a deferred state is that
the locks no longer prevent cyclic updates. For well defined values,
this works perfectly. But for something like floating points, this can
cause not-quite-the-same values to continue to propagate until they
stabilize. The 7guis-temperature-converter, for example, would
sometimes replace the currently editing text with an imprecise float
result computed from a roundtrip temperature conversion.

This change adds tracking to these deferred callbacks that ensures for
each "root" invocation of a dynamic's callbacks, all affected change
callbacks are invoked a single time in any given execution chain, taking
care to support divergent chains causing an individual set of callbacks
to be invoked more than one time as long as their individiaul call
chains have not yet invoked the callbacks.
The last change had a few expects fail in some examples. The issue was
caused by inserting the node incorrectly into the linked list.
The weak_clones are no longer necessary with the new change callback
model
Also fixed merge artifact -- the example moved but was restored during a
conflict.
@ecton ecton merged commit 0cefdde into main Jan 5, 2025
2 of 8 checks passed
@ecton ecton deleted the deadlocks branch January 5, 2025 18:39
@hydra
Copy link
Contributor

hydra commented Jan 14, 2025

It appears this is causing issues in my app after updating to a version of cushy with this PR.

More details + a video in discord, here: https://discord.com/channels/578968877866811403/842093272540643339/1328796671638765641

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants