Repository navigation
Avoid GC deadlock under Wine: add GC safe point to Dispatcher.ForBatched busy-wait - #3600
pm-lucarohde wants to merge 4 commits into
Conversation
…r Wine While waiting for other batches, ForBatched spun on TryCooperate() without ever making a system call. Since .NET 9, CoreCLR suspends threads for GC via special user-mode APCs (QueueUserAPC2), which Wine/Proton does not deliver to such a thread, so a GC starting during the wait deadlocked Game Studio. Use SpinWait when there is nothing to steal; it yields after a short spin, which lets the GC suspend the thread. sleep1Threshold: -1 avoids Sleep(1). Fixes stride3d#3599 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@dotnet-policy-service agree |
Eideren
left a comment
There was a problem hiding this comment.
The thread running this method is often (not always) the main thread. We definitely can spinwait, but we can't yield the timeslice here; games run on busywait or vblank loops, having a single thread hammered 24/7 helps reduce discrepancies in frame to frame interval.
Now this is definitely an issue that's not just localized around your specific setup as the gamestudio's threads in general do not necessarily follow this ethos. It spawns a ton of games and they all compete for the same resources with little to no orchestration.
I would like to improve that point, but there are more pressing tasks right now.
This change definitely introduces performance regressions, not as a constant cost, but one of stability under load, I would rather not accept it as is. Couple areas worth investigating instead:
- A non-yielding native method the GC can interrupt in your setup, seems unlikely but might as well validate that
- Listen for GC suspensions and let it yield then, same as above
- Detect this specific setup and add a specific path for it, preferably something that can be JITed away when running outside of that path.
The SpinWait back-off yielded the time slice, which is not acceptable here: the waiting thread is often the main thread and must keep spinning. Instead, keep the original busy-wait and, only when running under Wine, call a trivial regular P/Invoke (GetCurrentThreadId) each iteration. The P/Invoke round-trips through preemptive mode, so a pending GC can proceed without the special user-mode APC that Wine does not deliver, and the thread never yields. Wine is detected once via ntdll's wine_get_version export into a static readonly field, so tiered JIT removes the check on other platforms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
a431c1f to
d5bec23
Compare
|
Thanks, makes sense. I pushed d5bec23, which combines 1 and 3: the original busy-wait is back and never yields. 1. Native call: works. A regular P/Invoke ( 2. Listening for GC suspensions: not possible, as far as I can tell. Nothing in managed code exposes a pending suspension. The P/Invoke return in 1 is exactly that check, just done by the runtime. 3. Wine-only path: works. Wine is detected via Test with the real |
| // also helps prevent potential deadlocks from badly threaded code | ||
| while (Volatile.Read(ref batch.ItemsDone) < batch.Total && Volatile.Read(ref batch.ExceptionThrown) == null) | ||
| { | ||
| ThreadPool.Instance.TryCooperate(); |
There was a problem hiding this comment.
Shouldn't it test if TryCooperate() returned false before doing the Poll()?
There was a problem hiding this comment.
Shouldn't it test if
TryCooperate()returned false before doing thePoll()?
Good point! I’ve revised it.
|
The Wine hang seems fixed in Wine 11. Nevertheless, I think this small fix doesn't hurt and can prevent any other future regression. |
|
I'm having freezes when the GameEditor is building thumbnails (stuck at 247 in queue, with a empty project with all assets packs installed) despite running Wine 11.0. |
xen2
left a comment
There was a problem hiding this comment.
@MsEpsilon your freeze still happen with this branch?
If yes, one candidate: RootRenderFeature.GetOrCreateObjectNode (around line 155) has an empty while (Volatile.Read(...) == -2) { } spin. The thread it waits on calls objectNodes.Add, which can allocate and trigger a GC, so it could hit the same problem. Fine as a follow-up.
PR Details
Dispatcher.ForBatchedbusy-waits onThreadPool.Instance.TryCooperate()until all batchesare done. While the work queue is empty, this loop never makes a system call. Under
Wine/Proton with .NET 9+, CoreCLR suspends threads for GC with special user-mode APCs
(
QueueUserAPC2), and Wine doesn't deliver those to such a thread. So a GC that starts duringthis wait deadlocks the process. In Game Studio, this freezes the editor ~30–45 s after
opening a project.
When running under Wine, the loop now also calls a trivial regular P/Invoke
(
kernel32!GetCurrentThreadId) each iteration. The call goes through preemptive mode, so apending GC can proceed and the thread waits for it on return. It never yields the time slice.
Wine is detected once (
ntdll!wine_get_version) into astatic readonly bool, so tiered JITremoves the check on other platforms. The busy-wait itself is unchanged.
Testing: with the real
Stride.Coreunder GE-Proton11-6 / unpatched .NET 10.0.13, masterhangs 3/3 and this branch passes 3/3. Natively on Linux it passes too (details in the
comments).
Related Issue
Fixes #3599
Related: #3419
Types of changes
Checklist