From ed924e2beab0c084c467cb1836004eb2220c0dcc Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 19:45:21 +0000 Subject: [PATCH] Fix deadlock: ensure workers always remove from runningThreads on cancellation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When AbortBackgroundRecalc() is called before DoBackgroundRecalcDisplayList creates recalcDone, the abort cannot signal it. If workers subsequently see the cancellation at the early-exit checks and return without removing themselves from runningThreads, recalcDone.Count never reaches zero and recalcDone.Set() is never called, causing WaitOne() to block forever and t.Join() to hang indefinitely. Move the lock(runningThreads) cleanup block outside the cancellation guard so every queued worker unconditionally removes itself from runningThreads and signals recalcDone when it is the last one — regardless of whether it performed any actual work. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_014bFdnkBE83221SmUkyXHze --- CADability/Model.cs | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/CADability/Model.cs b/CADability/Model.cs index c8e03709b..2f7b06547 100644 --- a/CADability/Model.cs +++ b/CADability/Model.cs @@ -150,21 +150,25 @@ private void RecalcGeoObject(object state) { // hier wird die eigentliche Arbeit gemacht go.PrepareDisplayList(data.Precision); - lock (runningThreads) - { - runningThreads.Remove(data.RecalcID); // wirft keine Exception wenn nicht drin - // recalcDone hat einen Wert, wenn DoBackgroundRecalcDisplayList alle Objekte in die - // Queue gesteckt hat und auf das Ende wartet - if (recalcDone != null && runningThreads.Count == 0) - { // wenn der letzte thread fertig ist, nachdem alle in der Liste stehen - // wird recalcDone "signalisiert", d.h. alle Objekte sind jetzt berechnet - // und eine neue Displayliste kann schnell hergestellt werden - recalcDone.Set(); - } - } } } } + // Always remove from runningThreads regardless of cancellation. If a worker exits + // early due to cancellation without removing itself, DoBackgroundRecalcDisplayList + // can block forever on recalcDone.WaitOne() when AbortBackgroundRecalc ran before + // recalcDone was created and therefore could not signal it. + lock (runningThreads) + { + runningThreads.Remove(data.RecalcID); // wirft keine Exception wenn nicht drin + // recalcDone hat einen Wert, wenn DoBackgroundRecalcDisplayList alle Objekte in die + // Queue gesteckt hat und auf das Ende wartet + if (recalcDone != null && runningThreads.Count == 0) + { // wenn der letzte thread fertig ist, nachdem alle in der Liste stehen + // wird recalcDone "signalisiert", d.h. alle Objekte sind jetzt berechnet + // und eine neue Displayliste kann schnell hergestellt werden + recalcDone.Set(); + } + } } private class RecalcGeoObjectData