6. Threads and the call stack
When the program stops, it stops as a whole — every thread, reported together. Which thread triggered the stop does not limit what you can look at.
Threads
There is no separate Thread Status window: every live thread is a top-level row of the
CALL STACK section, with its frames underneath. Threads that announced a name through
TThread.NameThreadForDebugging show it: the debugger intercepts that
announcement as it happens and decodes the name, so a stack full of worker threads is
readable instead of a list of numbers.
Select any thread and you get that thread’s call stack, its frames’ locals, and its watches — regardless of which thread hit the breakpoint. For a deadlock or a race this is the whole job: stop once, then read every thread’s position.
The call stack
Frames are produced by a StackWalk64-based unwind, each with a function name
and a source line.
One detail that matters more than it sounds: caller frames are symbolicated at the return address minus one. A return address points at the instruction after the call, which on a line boundary belongs to the next statement — symbolicating it directly would report the wrong line for every caller in the stack. Backing up one byte lands inside the call, so the line you see is the call site.
Raw stack scan
Sometimes the unwinder has nothing to work with — no unwind data for the code in
question. This is mainly a 32-bit problem, where there is no .pdata to
consult. The call stack then ends early, and what you want to know is where the program
has been.
The raw stack scan is an opt-in fallback for exactly that: a brute-force sweep of the
thread’s stack for words that look like return addresses, resolved against every loaded
module. Turn it on with the rawStackScan launch flag, or toggle it from the
Call Stack view’s title bar during a session.
Vendingservice, everything below the divider is [raw?] output,
appended and greyed out rather than mixed into the real trace.
Results are marked so they can never be mistaken for a real unwind: [raw]
where the address is provably a call, [raw?] where there is no line table to
confirm it. Read them for what they are —
The presentation reinforces that. Swept entries are greyed out and always appended below the real frames, never mixed in among them. And selecting one shows no locals — not the locals of some neighbouring frame, which would be worse than nothing. There is no frame there to read; the address is just a number that was found on the stack.
Turning the scan on or off confirms itself in the status bar, and the confirmation repeats the warning rather than just saying “on”. That is deliberate: the feature is only dangerous if you forget what the extra entries mean.
Stepping and the other threads
Hover a thread’s row in the Call Stack panel and it grows its own inline controls —
continue and the three step commands, Step Out among them — so you can act on
that thread directly from its row, without touching the floating debug toolbar at
all. Using one also focuses that thread, which is what then makes the ordinary toolbar
track it too.
Step Out by its tooltip; the others follow the
same continue/step-over/step-into order as the floating toolbar.A step command targets one OS thread. Every other thread is explicitly suspended for the duration of the step — including threads created while the step is in progress, and including the whole of a call you step over — so the program cannot run away underneath you between one line and the next. If the stepped thread exits mid-step, everything is thawed rather than left frozen.
When the stepped-over call needs another thread
Freezing has one consequence you will meet sooner or later: the call you are stepping over
may be waiting for one of the threads you just froze. Application.Initialize
on an application whose start-up hands work to a worker is a typical case, and so is a
constructor that takes a lock another thread holds. Left alone, that step would never land.
The debugger watches for it. While a step is quiet it probes the stepped thread every 250 ms, on two tiers:
-
A lock with an owner. If Windows can name what the thread is waiting on
— a critical section, a mutex, a
SendMessage, another thread — and the owner is one of the frozen threads, the others are released at once. -
An object with no owner. An event, a semaphore, I/O: nobody can say who
will signal it. If the stepped thread has consumed no CPU time for
stepIsolationReleaseMs— three seconds by default, deliberately long so that a slow external event is not mistaken for a deadlock — the others are released. A call that is busy computing never triggers this.
Every release is announced once in the Delphi Debugger output channel, naming the reason (a critical section held by thread 4712, which this step froze, or an object with no owner the debugger can name), so you always know when the isolation was given up. The other threads run only until the step lands; the next single-stepped instruction freezes them again.
The Auto-Release button
Sometimes the contention is what you are debugging: two threads cooperating on a lock, and you want to watch exactly one of them without the other moving. For that the Call Stack title bar carries a button next to the raw-scan toggle, whose icon shows the current state:
- Open lock — Auto-Release: ON (the default). A stepped-over call found waiting on a frozen thread releases the others as described above. Click to turn it off.
- Closed lock — Auto-Release: OFF. Strict isolation: every other thread stays frozen for the whole step, even if the stepped-over call waits on one of them. If that happens, the step simply does not land; press Pause to break in — Pause always works, it releases the threads first. Click to turn auto-release back on; a step that is already stalled is released as soon as the detector runs.
-
Crossed circle — Step Isolation: none. The session was started
with
"stepIsolation": "none"and never freezes other threads for a step, the RAD Studio IDE’s behaviour. The button is informational.
The switch takes effect immediately for the running session and shows the state it just
selected in the status bar. The same three behaviours can be chosen up front in
launch.json with stepIsolationReleaseMs (0 starts
the session with auto-release off) and stepIsolation — see the
reference.
The threading model
covers the mechanism.