Threading¶
Long-running Python tasks (network requests, file I/O, MCP servers) must run in background threads to avoid freezing TouchDesigner's UI and cook cycle.
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 inside worker threads, use plain
threading.Threadinstead of ThreadManager - 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