skip to content

A guard holding an export lock is declared at the top of a long function. What does that cost, and how would you fix it?

level: seniorimportance: nice to knowfreq 28%

answer

  1. held for exactly its scope
  2. hoisting widens the critical section
  3. slow work outside the block
  4. inner block sizes the hold
  5. unnamed guard dies that statement

basics

~20 s

The lock is held for the whole function, including slow work it does not protect, so unrelated callers queue behind a network round trip. Size the scope to the critical section: put the guard in an inner block.

solid answer

~50 s

Scope-bound release is exact, and that cuts both ways: the resource is held for **precisely** the scope you chose, so choosing a scope that is too big is a real cost rather than a style issue. A lock guard at the top of a function that also builds a payload, writes a file and performs a network send holds the lock across all of it, and every other caller of that critical section waits behind work the lock was never meant to cover. The fix is to introduce an inner block — or extract the protected step into its own function, which is the same thing — so the guard is constructed and destroyed around the protected step alone. Do the slow work outside it, on local data the lock was there to hand you safely.

code

pseudocode · 10 lines
pseudocode
function publish(batch):
    payload = build(batch)          # no lock needed yet

    begin scope                     # inner block
        guard lock_guard = acquire(export_lock)
        previous = swap_in(payload) # the only step the lock protects
    end scope                       # lock released here, before the slow part

    send_to_downstream(previous)    # network round trip, runs unlocked
    return previous

go deeper

for a junior

The resource is held for exactly as long as the block containing its guard. A bigger block means a longer hold; that is the whole rule.

for a middle

Explain how to shrink it: build inputs before the block, do the smallest protected operation inside it, and run serialising and sending after it on a local copy.

for a senior

Name the cost in production terms — waiting callers, a starved connection pool, a descriptor limit — and fix it by moving the scope rather than adding a mechanism.

for a principal

Set the expectation that critical-section size is a reviewable property with a number attached, and that contention fixes start by measuring hold time, not by adding locks or retries.

## Exactness is the feature and the trap The guarantee is that the resource lives for exactly the scope holding the guard. That is what makes the release deterministic, and it is also why a badly chosen scope is a defect you can measure. Two directions of error exist: - **Scope too large**: the guard is hoisted to the top of a long function 'so it is not forgotten'. The lock is now held across payload construction, disk writes and network calls, so the critical section's hold time is dominated by work that has nothing to do with the shared state. Contention on that lock now scales with the latency of a remote system. - **Scope too small**: the guard is created as an unnamed temporary that is destroyed at the end of the statement that created it, so the lock is released before the block that was supposed to run under it. The code reads as if it is protected and is not, and the failure is a rare data race rather than a crash. ## Sizing the scope The fix for the first is mechanical: wrap the protected step in an inner block and declare the guard inside it. Everything that does not touch shared state moves outside. - Compute what you can **before** taking the lock; take it with the inputs ready. - Under the lock, do the smallest operation that keeps the invariant: swap a pointer, append to a structure, read a consistent copy out. - Do the slow, failure-prone work — serialising, writing, sending — **after** the block, on the copy you took. | where the guard is declared | lock held for | effect on other callers | |---|---|---| | top of the whole function | build, protected step, write, send | queue behind a remote round trip | | inner block around the protected step | the protected step only | queue behind an in-memory operation | | unnamed temporary in a statement | that statement only | nothing is actually protected | ## Why not just release early by hand? Some guard types offer an explicit early release, and it is occasionally the right tool — a long loop that must drop a lock between iterations, for example. But reaching for it as the normal answer reintroduces exactly the problem the guard removed: a release that a future early return or error path can skip. An inner block cannot be skipped, because leaving it by any route is what triggers the release. Prefer the block; keep the manual release for the case where the hold genuinely must end in the middle of straight-line code. ## The same question for other resources Lock hold time is the loudest case because contention is visible in latency percentiles, but scope sizing matters for anything scarce: 1. **Pooled connections** — a guard held across an unrelated computation keeps a connection out of the pool; under load the pool starves while most of its connections are idle in someone's scope. 2. **File handles** — a guard in an outer scope inside a loop keeps every iteration's handle open until the loop ends, which is how a process hits a descriptor limit with a program that 'closes everything'. 3. **Large buffers** — held to the end of a long function, they raise peak footprint for the whole call rather than for the phase that needed them. ## What to say in an interview The strong answer names the cost in the units the system is measured in — waiting callers, pool starvation, descriptor limits, peak footprint — and then gives the fix as a scope change rather than a new mechanism. The weak answer treats the hoisted guard as a style preference, or proposes a second lock, a timeout or a retry to work around contention that a three-line block edit removes. And the answer that shows real experience mentions the opposite error too: a guard that was never bound to a name protects nothing, and that bug looks correct in review.

  • What happens to a guard that is created but never bound to a name?
    It is a temporary, so it is destroyed at the end of the statement that created it — the lock is taken and released on the same line. The block that follows then runs unprotected while reading as though it were protected, which makes this a rare data race rather than a visible failure.
  • Is extracting the protected step into its own function equivalent to an inner block?
    Yes: a function body is a scope, so a guard declared inside it is released when the function returns by any route. It is often the better form, because the name documents what the critical section is, and nothing unrelated can drift into it later.
  • The protected step is inside a loop that runs for minutes. Where does the guard go?
    Inside the loop body, so the lock is taken and released once per iteration and other callers interleave. Hoisting it outside the loop holds the lock for the whole run; the cost of re-acquiring per iteration is almost always smaller than the contention of holding it throughout.

saying these in an interview costs you the question

  • Treats a hoisted guard as a style preference, not a contention cost
  • Proposes retries or timeouts instead of shrinking the critical section
  • Says a manual early release is always equivalent to an inner block
  • Keeps a pooled connection guard alive across unrelated computation
  • Declares a file-handle guard outside a loop that opens one per iteration
  • Believes an unnamed temporary guard protects the block that follows it