Diagnosing TCriticalSection UI Deadlocks in Delphi
A technical diagnostic guide for resolving TCriticalSection deadlocks in Delphi that cause UI freezes, featuring a cause-and-effect table and specific code fixes.
09 Aug 2025, 14:17 UTC

The Symptom: UI Freeze with Zero CPU Usage
A common failure mode in multi-threaded Delphi applications is a total UI freeze where the window stops repainting and ignores all user input, yet the process CPU usage drops to near zero. This indicates a deadlock rather than an infinite loop. The core problem is that TCriticalSection wraps the Win32 CRITICAL_SECTION, which lacks a timeout mechanism. If the main thread acquires a lock and then enters a blocking wait, the message pump stalls, and any worker thread attempting to enter that same section will block indefinitely.
Cause and Diagnostic Table
| Symptom | Likely Cause | Diagnostic Check |
|---|---|---|
| Freeze occurs exactly when a modal dialog opens | Lock held across TForm.ShowModal on the UI thread |
Search for Enter calls preceding ShowModal |
| Freeze occurs after background task starts | Worker thread blocks on a lock held by the UI thread during a WaitMessage |
Check UI thread stack for WaitMessage or GetMessage |
| Intermittent freeze without clear UI trigger | Enter/Leave mismatch (exception or early return) |
Audit paths for missing finally blocks |
Freeze during TThread.Synchronize |
Lock held while calling Synchronize (Priority Inversion) |
Search for Enter calls wrapping Synchronize |
Ordered Diagnostic Checks
1. Analyze the Thread Stack
Run the application in a debug build and pause execution during the freeze. Inspect the main thread stack. If you see TCriticalSection.Enter (or the underlying Win32 wait) while the thread is also associated with WaitMessage or GetMessage, the UI thread is waiting for a lock it cannot acquire because the message pump is frozen, or it is holding a lock while waiting for a message that can only be processed if the lock is released.
2. Audit Lock Pairing
Review all TCriticalSection usage. Ensure every Enter is paired with a Leave inside a try..finally block. Early returns or unhandled exceptions that skip the Leave call will leave the section locked forever.
3. Identify Blocking UI Calls
Search the codebase for any blocking calls made while a lock is held on the main thread. These include:
TForm.ShowModalApplication.ProcessMessages(in some recursive scenarios)TThread.SynchronizeorTThread.Queue(if the worker thread is also waiting on the same lock)
4. Check for Synchronize Nesting
Verify that you are not calling TThread.Synchronize from within a locked region. On Windows 7 and later, nesting a critical section inside a synchronization call can lead to priority inversion, where the UI thread is preempted or blocked, extending the freeze duration.
Fixes Based on Findings
Finding: Lock held across Modal UI
The lock must be released before the modal dialog is displayed. If the dialog needs the shared data, copy the data to a local variable first.
// Incorrect: Lock held during modal wait
CS.Enter;
try
Form.ShowModal; // DEADLOCK RISK
finally
CS.Leave;
end;
// Correct: Copy data, release lock, then show UI
CS.Enter;
try
LocalData := SharedData;
finally
CS.Leave;
end;
Form.Data := LocalData;
Form.ShowModal;
Finding: Enter/Leave Mismatch
Standardize all lock acquisitions using the try..finally pattern to guarantee release regardless of the execution path.
CS.Enter;
try
if NotValid then Exit; // Without finally, this is a leak
ProcessData;
finally
CS.Leave;
end;
Finding: Worker Thread Deadlock
If a worker thread is blocking the UI, move the heavy lifting to TThread.Execute and use TThread.Synchronize only for the final UI update. Ensure the UI thread never holds a lock while calling Synchronize.
Verification and Limitations
To verify the fix, create a minimal reproduction case: a form that acquires a TCriticalSection and immediately calls ShowModal. Confirm the UI hangs. Apply the "copy-and-release" fix and verify that the window remains responsive.
For production environments, use Windows Performance Analyzer (WPA) to capture wait stacks. Ensure that no wait chains cross the boundary between a worker thread and the main message pump.
Limitations: TCriticalSection does not support timeouts. If your architecture requires the ability to "give up" after a certain period, you must replace it with TMonitor.TryEnter or a Win32 Mutex.
Escalation Criteria
Escalate the issue to a senior architect if:
- Deadlocks persist after all locks are removed from the UI thread's wait paths.
- The application uses multiple nested critical sections in different orders (Lock-Order Inversion).
- The freeze occurs inside a third-party binary component where the source code is unavailable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.