Threading¶
Long-running Python tasks (network requests, file I/O, MCP servers) must not block TouchDesigner's single main thread, or the UI and cook cycle freeze - and touching a TD object from a worker thread crashes TD outright.
Choose the lightest mechanism; threading is the last resort. Prototype synchronously to prove the logic, then: (1) fast TD-only work -> run it inline on the main thread; (2) HTTP fetch -> the TD-native Web Client DAT, which does async networking with no hand-rolled threads and delivers onResponse on the main thread (parse with a JSON DAT and bridge to channels with a DAT to CHOP); (3) long main-thread work -> chunk across frames with run(); (4) blocking pure-Python work -> the Thread Manager below (prefer the Palette Thread Manager Client); (5) a long-lived loop or a pure-Python sub-task -> a threading.Thread that touches no TD object and never calls run(). Reach for a thread last, not first.
Thread Manager¶
TouchDesigner provides op.TDResources.ThreadManager — a wrapper around Python's threading module with TD-safe hooks.
Critical Rule
Never access TouchDesigner objects (OPs, COMPs, parameters) from a worker thread. All TD operations must go through hooks that execute on the main thread.
Basic Usage¶
# Create a task
task = op.TDResources.ThreadManager.TDTask(
target=my_background_function, # Runs in worker thread (no TD access!)
args=(arg1, arg2), # Passed to target
SuccessHook=on_success, # Main thread — called when target returns
ExceptHook=on_error, # Main thread — called on exception
RefreshHook=on_refresh, # Main thread — called every frame while running
)
# Enqueue it (runs in worker pool)
op.TDResources.ThreadManager.EnqueueTask(task)
# Or run in a dedicated thread (outside the pool)
op.TDResources.ThreadManager.EnqueueTask(task, standalone=True)
Key Concepts¶
TDTask¶
A unit of work with a target callable and optional hooks:
target— function to run in background (no TD access)SuccessHook— called on main thread when target returnsExceptHook— called on main thread on exceptionRefreshHook— called every frame on main thread while running
InfoQueue¶
Thread-safe channel from worker thread to main thread:
# In worker thread:
worker_thread.InfoQueue.put(data)
# In RefreshHook (main thread):
def on_refresh(task):
while not task.thread.InfoQueue.empty():
data = task.thread.InfoQueue.get()
# Process data with TD access here
Standalone vs Pool¶
- Pool (default): Up to 4 worker threads for short-lived tasks
- Standalone (
standalone=True): Dedicated thread for long-lived tasks like servers
run() — Delayed Execution¶
The run() function defers Python execution — essential for timing-sensitive operations:
# Delay by frames:
run("op('/project1/base1').cook(force=True)", delayFrames=1)
# Delay by time:
run("print('done')", delayMilliSeconds=500)
# End-of-frame execution (after current cook cycle):
run("op.Embody.Update()", endFrame=True)
# With a callable:
run(myFunction, arg1, arg2, delayFrames=5)
# Relative to a specific operator:
run("me.cook(force=True)", fromOP=op('/project1/base1'), delayFrames=1)
Thread Safety Pitfalls¶
- Never access
op.TDResources.ThreadManagerfrom a worker thread — it's a TD COMP, and accessing it triggers a THREAD CONFLICT - For sub-tasks spawned INSIDE an existing worker thread (where
EnqueueTaskis itself off-limits), use a plainthreading.Thread- and its body, like any worker, must touch ZERO TD objects and must never callrun()/td.run()(which raisestdErrorfrom a worker). This is NOT a recommendation to start background work withthreading.Thread; for that, prefer the Web Client DAT (fetches) or the Thread Manager (blocking pure-Python) - Worker pool is limited to 4 threads (CPU count cap) — use
standalone=Truefor long-lived tasks - All data crossing the thread boundary must go through
InfoQueueorQueueobjects