A camera class that grabs frames in a background thread behaves all shift, then kills the whole process the moment you close it. The crash lands inside the SDK's native library, far from any line you wrote, and it reproduces maybe one run in five. That signature points at one cause: something stopped the pipeline while the grab thread was still inside it. This guide gives the shutdown order that removes the race, a stop-safe loop you can copy, the join timeout to set at any frame rate, and a 1,000-cycle test that proves a clean exit. The pattern covers GigE and USB3 area scan cameras and 3D sensors whose SDKs follow the same start, wait and stop shape.
Why does stopping the grab thread segfault the pipeline?
The segfault happens because stop and the grab thread race: the thread is still inside the SDK's frame-wait call holding device buffers when the stop call tears down the same memory, and freeing memory another thread is using is undefined behavior.
Walk through the class in the classic failure. The constructor builds the pipeline and starts acquisition, a worker thread loops on frame waits, and the stop method sets a flag and closes the device in the same breath. The flag does nothing until the thread comes back around to the loop top, so the close executes while the worker is still inside a frame wait, holding buffers the SDK allocated. Freeing memory underneath a live native call is undefined behavior, and at exactly this spot it usually means a segmentation fault.
There is a second participant even in code that looks single-threaded: every machine-vision SDK runs its own receive thread inside the library. Your stop call races that thread whether or not you wrote a worker of your own — but the worker you did write widens the window from microseconds to a full frame-wait timeout.
Two details confirm the diagnosis. The crash comes at shutdown, not mid-stream. And it is intermittent, because the outcome tracks where the worker sits in its loop when the stop lands: hit the gap and the process dies, miss it and the same code runs for weeks without a complaint. A related device-state failure, buffers still marked busy at stop, has its own walkthrough in stop camera acquisition with busy buffers.
What order shuts a camera pipeline down safely?
Use a fixed four-step order, all from the owner thread: set the loop flag false, join the grab thread with a timeout of the frame-wait timeout plus three frame periods, then call the SDK stop, then release the device — never while a join is still pending.
The arithmetic at 30 fps: the frame period is 1000 / 30 = 33.3 ms. With the frame-wait timeout set to 1000 ms, the join budget is 1000 + 3 × 33.3 = 1100 ms, so set the join timeout to 1500 ms and keep the difference as margin for scheduling jitter.
- Set the run flag false. Nothing else changes; the worker exits at its next top-of-pass check.
- Join the worker with the timeout from the formula. A still-live thread after that is an error to raise, not a reason to wait longer.
- Call the SDK stop-acquisition function. The device is idle now, so no internal thread holds a buffer.
- Release or close the device handles. Where an SDK splits stop and close, close follows stop; reversing them rebuilds the race you just removed.
All four steps run from one thread — the one that created the pipeline. If a GUI callback and a shutdown routine can both reach the stop method, guard the whole sequence with a lock so the second caller queues behind the first; two simultaneous stops are the same race wearing a different hat. If you are building the acquisition side from scratch, the hardware trigger grab loop in Python pairs with this shutdown as-is.
What does a stop-safe grab loop look like in code?
A stop-safe loop has four properties: every frame wait carries a timeout of about one second, the loop checks a plain boolean flag at the top of each pass, and the SDK stop runs only after the thread has joined. API names differ per SDK; the order does not.
import threading
class CameraGrab:
def __init__(self, dev):
self.dev = dev # SDK device handle, created by the owner thread
self.running = False # bool, loop flag
self.thread = None # threading.Thread or None
self.latest = None # last good frame, or None
def start(self):
if self.thread is not None:
return
self.running = True
self.dev.start() # SDK start-acquisition call
self.thread = threading.Thread(target=self._loop)
self.thread.start()
def _loop(self):
while self.running:
frame = self.dev.get_frame(timeout_s=1.0) # bounded wait
if frame is None:
continue # timeout: loop re-checks the flag
self.latest = frame
def stop(self):
self.running = False # 1: ask the loop to exit
self.thread.join(timeout=1.5) # 2: wait, from this thread
if self.thread.is_alive():
raise RuntimeError("grab thread stuck in the frame wait")
self.dev.stop() # 3: now stop acquisition
self.dev.close() # 4: release the handle
self.thread = None
Every name here is a placeholder for your SDK's own: start acquisition, wait for frame with timeout, stop acquisition and close exist under other names in every machine-vision library, and the 1.5 s join follows the 1000 + 3 × frame-period budget from the section above. Three details carry the fix. The wait is bounded, so a stalled camera cannot trap the loop. The flag is a plain bool read at exactly one spot, so no lock is needed to stop. And stop() raises when the thread outlives the join, which turns a silent race into a loud, debuggable failure on your bench instead of a crash at a customer's site.
Why does thread.join() hang when the camera stalls?
join() hangs because the grab thread sits blocked inside the frame-wait call, which returns only when a frame arrives or its timeout expires; with no timeout that is forever. Give every wait a bounded timeout near one second and size the join from the formula above.
Unplug the camera mid-run and watch it happen: an unbounded wait never returns, join never returns, and the application looks frozen while the camera is simply gone. A timeout on the join alone does not cure this — it just converts a hang into a raised error while the thread stays stuck.
Three steps, in this order, when you inherit a loop with an unbounded wait:
- Set the frame-wait timeout in the SDK's acquisition settings; every machine-vision library exposes one, and 1000 ms is a sound default.
- Interrupt the wait the documented way before joining — the clean interrupt of a hardware-trigger grab timeout walkthrough shows the pattern for triggered cameras.
- If the thread still outlives the join, log it and leave it leaking: the device already in use error is the next symptom, and a process you can still shut down beats a segfault you cannot.
What you must not do is call the SDK stop from outside while the worker is trapped in the wait — that is the original race with a hang attached.
Can two threads call the camera SDK at the same time?
Treat the SDK as single-owner: one thread creates, starts, stops and closes the device, while other threads only read frames that the grab thread copied out. Most SDK objects are not documented as thread-safe, and the internal receive thread counts as a second party you must not race.
Start from the fact that the library already runs one thread you did not write: the receiver that turns network or USB packets into frames. Your worker is therefore never alone, and every extra thread that touches the device object widens the race. The owner thread — the one that called start — is the only one allowed to call stop, close, or any setter on the device.
Consumers get copies, not the device. The worker pushes frames into a queue with a small maximum size — two entries is enough: one frame of slack covers scheduler jitter, two covers a consumer that stalls for one frame period. Use the thread-safe queue class from your language's standard library, built for exactly this producer-consumer handoff, rather than a bare list or dict swap. Stale or repeated frames in that queue are a separate failure mode — latest image only returns duplicate frames covers the drain side. On a multi-camera rig, give each device its own worker and its own owner thread; sharing one device object between two grab loops is a race you cannot lock your way out of, because the SDK's internal state machine sees interleaved commands.
How do you prove the shutdown is fixed before you ship?
Run a 1,000-cycle start/stop test with a random 0-100 ms delay inside the loop and require a clean process exit every time; a race that fires once per few thousand shutdowns shows up inside ten minutes at these numbers.
import random, time
grab = CameraGrab(dev)
for i in range(1000):
grab.start()
time.sleep(random.uniform(0.0, 0.1)) # land on every spot in the loop
grab.stop()
print("1000 start/stop cycles, process alive")
Each cycle costs about 0.30 s to start acquisition, 0.05 s of average sleep, and 0.02 s to stop and close, so 1,000 cycles take 1,000 × 0.37 s = 370 s, six minutes. The random delay is the point: a fixed delay can land on the same safe spot in the loop every pass and prove nothing. Pass means four things: the process exits with code 0, no native crash appears, every join returns inside the 1500 ms budget, and the device opens again after the last cycle.
Add the failure path in the same session: pull the camera cable once mid-loop and confirm the wait times out, the flag ends the loop, and stop still runs clean. In Python, enable the faulthandler module so a native crash prints a traceback instead of dying silently; in C++, rebuild one pass with a thread sanitizer, which reports the data race directly at the two call sites.
The rig is ordinary on purpose: any camera with a start/wait/stop SDK works, and these three cover the common GigE and USB3 options:
- Sensor format
- 1/1.7"
- Lens mount
- C-mount
- Sensor format
- 1/1.7"
- Interface
- USB3
- Sensor format
- 1/1.8"
- Lens mount
- C-mount
Scale the same test to several cameras at once on a controller sized for it: the MV-VC3301H-128G66 pairs an Intel Core i3 processor and 8 GB of memory with 5+ LAN ports for camera connections.
- LAN ports
- 5+ LAN ports
- USB 3 ports
- 3-4 USB 3 ports
- Light outputs
- 4 built-in channels
- Memory
- 8 GB
Pin this list next to the rig before you call the shutdown done:
- Frame wait carries a timeout near 1 s.
- The loop reads one boolean flag and touches no SDK call on exit.
- Stop runs only after join returns, from the owner thread.
- Join timeout = wait timeout + 3 × frame period (1500 ms at 30 fps with a 1 s wait).
- 1,000 start/stop cycles with a random 0-100 ms delay end with exit code 0.
- One cable pull mid-loop ends in a clean stop, not a hang.
When is the crash not your ordering bug?
If the order above still ends in a native crash, suspect the SDK and firmware build next: install the versions the vendor documents as compatible before you debug deeper, because a use-after-free inside the library is beyond anything your shutdown order can fix.
Three device-state failures produce crashes that look identical at the stack trace. Buffers still marked busy at stop come from consumers that never released their references — the busy-buffers walkthrough earlier in this page. A start without a matching stop leaves the acquisition state machine wedged, and the single frame acquisition still busy article traces that path. Both are fixed in application code; neither is your threading.
A crash that survives all three fixes sits inside the SDK or its driver build. Install the SDK and camera firmware versions the vendor or its distributor ships as a matched pair, and re-run the 1,000-cycle test before you write a single line of workaround. The ordering fix costs an afternoon; chasing a library bug without it costs a week, because you cannot tell your bug from theirs until your side is provably clean.
That is the whole fix: one owner, one flag, join before stop. If you are sizing the rig the loop runs on, the camera finder filters by interface, resolution and shutter, the vision controllers category filters by LAN port count and built-in light channels, and the systems topic page collects the rest of the grab-loop patterns. For a review of a shutdown path that still misbehaves, the contact page is the way in.
Questions
Does setting the run flag to False stop the camera immediately?
No. The flag only ends the loop when the current pass reaches the loop top again, so grabbing continues until the in-flight frame wait returns. With a 1 s wait timeout the worst case is about one second, and the SDK stop still runs only after the join comes back. That delay is the price of a clean exit, and it is the right price.
Can I mark the grab thread as a daemon and skip the join?
You can, and it moves the crash instead of fixing it. Python cuts daemon threads off abruptly at interpreter exit, which can leave the SDK mid-call with the same torn-down state you started with. Reserve daemon=True for loops that must never block exit; a camera worker holding device buffers is the opposite of that.
Would a separate process avoid the segfault entirely?
It contains it rather than prevents it. If the grab loop runs in its own process, a crash takes the child down while the main process survives, and the handoff between the processes stays inside documented, thread-safe shared objects. The cost is serialization of every frame: a 12 MP 8-bit stream at 30 fps is about 4000 × 3000 × 30 ≈ 360 MB/s across the boundary, so measure before you commit.
Does the shutdown order change for hardware-triggered cameras?
The order is identical; only the wait changes. A triggered camera can idle far longer than one frame period between triggers, so set the frame-wait timeout against the longest trigger gap you allow, not against the frame rate. The camera waiting for a trigger still owns buffers, so the rule stands: join first, stop second, release last.
Why did the camera run clean for hours and crash only at program exit?
Because the race window only opens when a second caller appears. During steady grabbing, only the worker touches the SDK, so nothing collides. At exit, the interpreter's own teardown or your cleanup routine becomes that second caller while the worker is still inside a frame wait. Hours of clean runs followed by a crash on the last line is the classic signature of this ordering bug.



