Threading and TouchDesigner
Today we are introducing the Thread Manager and a number of tools that work with it.
The main obstacle to overcome is that TouchDesigner's main thread needs to continuously run, as it is the main thread that runs the editor, moves the timeline forward and generates frames.
Python operations can block the main thread, and this blocking will result in frame drops or worse it could hang or crash TouchDesigner.
The Thread Manager simplifies threading and task handling in TouchDesigner for Python developers.
Furthermore, for users who want an easy learning curve into threading, they can look at the Thread Manager Client available in the Palette. It’s a simple callback-oriented component that wraps around the Thread Manager.
Various debugging tools are available, such as the Threads Monitor COMP and advanced logging relying internally on the Logger COMP.
Advanced users will want to read our wiki in depth as well as the extension page of the Thread Manager. You can rely on the Thread Manager from your own scripts and extensions using op.TDResources.ThreadManager
.
An easier introduction to threading in TouchDesigner: The Thread Manager Client.
ThreadManager
and find the threadManagerClient
COMP.Run In Thread
, or call the pulse()
method of that Pulse parameter: n.par.Runinthread.pulse()
where n
is equal to the Thread Manager Client COMP.ThreadManager
and find the threadsMonitor
COMP and read on.Introducing the Threads Monitor, your best friend to spy on the Thread Manager.
This is your default view of the Threads Monitor COMP if no thread is currently running.
Go back to your threadManagerClient
COMP and click on Run In Thread
again while looking at the threadsMonitor
COMP. You should see a task appearing in the bottom half of the threadsMonitor
viewer, this area shows all the tasks being queued.
If you look at the lister at the top of the viewer, this task was picked by a worker thread and is currently being executed, this area shows all worker threads currently running.
Ok - spam that pulse button like you are still playing Cookie Clicker, I know you want to.
See all those tasks being queued ? You should have four being picked and ran in the four worker threads of the Thread Manager. Four is the default number of worker threads, but this can be changed easily in settings.
If you have more than four tasks in the queue, you see the additional tasks patiently waiting to be executed in the tasks list. After a little while, all the tasks should be processed.
And there, in just a few clicks, you saw the magic happen with the Thread Manager. No need to implement all that logic yourself because the most generic and generalist approach is covered for you.
Let’s get back to the threadManagerClient
COMP and walk through the callback template.
Implementing your own methods in the Thread Manager Client COMP Callback.
There are two things we can note from the screenshot above.
-
The callback DAT template is fully commented. There is important information that you should read carefully through.
-
There is prefilled code to demo usage which will run when clicking the
Run In Thread
pulse.
Setup
method is where you should prepare your task and possibly gather and prepare data to be passed and ingested by the thread.This method should build a payload and return it to be used by the RunInThread
method.
! The payload should not contain any TouchDesigner object. You should make any object a generic python object. i.e. In the case of passing a parameter to the thread,
n.par.Somecustompar
, make sure to evaluate the parameter and pass only its value, or a JSON representation of this parameter, serialized.valueToPass = n.par.Somecustompar.eval()
or see TDJSON for serialized data from TouchDesigner objects to JSON.
The RunInThread
method is the method that is likely blocking and needs to be executed in a thread as to not disrupt the main TouchDesigner thread. Make sure to never access any object from the main thread during execution of this method, whether it’s for reading or writing data.
If you look at the template content, quite a bit is going on.
First, we are reading from the payload that was created in our Setup
method.
Once the values passed are retrieved, we build a loop (purposedly) that will block our thread. This is really just for demonstration purpose.
The loop will loop for X time where X is the value of count in our payload. During each loop, it will sleep for X seconds where X is the value of sleepDuration
in our payload.
Because of the design of that method to be threaded based around a loop, we are making use of this specific case to demo the use of refresh payloads.
Once per loop iteration, we are adding a payload to the Refresh Queue, calling the AddInRefreshQueue
method.
The Refresh Queue is a Thread Queue where payloads can be added from the thread, to then be dequeued in the main thread. Again, ALWAYS make sure to only pass safe objects, as in, purely Pythonic objects that do not interact with TouchDesigner’s main thread.
Additionally, we are setting an InfoDict
for our thread info queue. This is optional, but it can be used for the user to pass states to be displayed in the Thread Monitor COMP, or my favorite one, passing Progress
values when iterating through a loop.
At the very end of our threaded method, we set a Success Payload, calling the SetSuccessPayload
method.
The next method is OnRefresh
. OnRefresh
is called on TouchDesigner’s main thread. Read that again, it’s important to understand it's on TouchDesigner’s main thread. Really there is only one method in that callback that is called in a thread, all the other methods are called from the main TouchDesigner thread.
In OnRefresh
, if you passed a refresh payload or more from the RunInThread method, the refresh payload that was possibly dequeued is passed to the OnRefresh callback as an argument and used in anyway you like. Otherwise, if no refresh payload was passed, the method is called at least once per frame.
Finally, we have two methods: OnSuccess
and OnExcept
. Both are called from the main thread ( again ;) ).
OnSuccess
is called last when everything completed smoothly. If a success payload was set at the very end of the threaded method execution, the payload is passed and can be used in anyway you like.
OnException
is called at any time during the threaded method execution if an issue occurred and an exception was raised. You can give it a try by going back to the RunInThread
method and uncommenting the last bit of the for loop starting with if i == 2
and what's underneath.
Ok, that covers the Thread Manager Client and the basic usage of the Thread Manager.
If you have a look at the extension page, you can get an idea of the things that are exposed for you to use within the callback as well, although most if not all are demoed in the template.
! It’s good practice to use thread locks (
threading.Lock()
) or conditions (threading.Condition()
) when accessing (read or write) objects that can be used in different threads. If you add an attribute to an extension which is storing reference to (non-TouchDesigner) data and that attribute is accessed from both the threaded method and the main thread, you might consider a lock.
Unleashing threading with the Thread Manager extension.
This section is for advanced users that wish to implement threading manually in their project with the support of the thread manager.
!!! As a starting point, it is strongly advised to fully read the documentation of the Thread Manager page, read the Thread Manager Extension documentation as well as use (showcased in previous section) the Threads Monitor.
TDTask
The starting point to integrating the Thread Manager in your application is to create a task.
Considering the following threadManager = op.TDResources.ThreadManager
you would create a task by creating a new TDTask
, exposed in the Thread Manager Extension, myNewTask = threadManager.TDTask(…)
.
If we look at the Thread Manager Extension documentation, we can see that the task needs at the very least a target
method to be specified.
The target method is the method that will be ran in a thread once the TDTask
is picked up by the Thread Manager.
Additionally, users can specify a SuccessHook
, an ExceptHook
, RefreshHook
, pass args
as a tuple
or kwargs
as a dict
.
Here is an example of an integration passing a target, setting all hooks, and passing arguments:
myTDTask = threadManager.TDTask( target=self.LoadModel, SuccessHook=self.LoadModelSuccess, ExceptHook=self.LoadModelExcept, RefreshHook=self.LoadModelRefresh, args=(modelPath,) )
If I consider the method
def SomeTestThreaded(self): myTDTask = threadManager.TDTask( target=self.SomeTest ) threadManager.EnqueueTask(myTDTask) print('SomeTest started') return
With the target being a blocking method as follow:
def SomeTest(self): for i in range(10): print(f"Iteration {i + 1}") time.sleep(1) # Pause for 1 second return
We can see that in my initial calling method where the task is created, that we call enqueue
threadManager.EnqueueTask(myTDTask)
This is all that is really needed to run a blocking method as a task in TouchDesigner with the new Thread Manager.
When calling this test method directly, we can see it in the threadsMonitor
COMP for about 10 seconds. In the task lister, only the task ID, the name (the target method) and the ID of the thread that picked the task will be displayed.
For additional members and methods on TDTask
objects, you should read the TDTask
section of the Thread Manager Extension wiki.
Enqueue
Let’s talk a bit more about EnqueueTask
, because we can customize the behavior.
We always want to pass a TDTask
as the first argument. However, we can use the force
argument, or the standalone
argument.
When setting force
to true
, the TDTask
will be immediately sent to an available thread in the pool of worker threads. If no worker thread is available, a new TDThread
will be created for the TDTask
to run immediately.
When setting standalone
to true
, a standalone TDThread
will be created for the TDTask
to run immediately, regardless of whether a thread in the pool of worker threads was available or not. This guarantee the task to run in its own TDThread
.
standalone
takes priority upon force
, it means that if both of the arguments are set to true
, or force
is set to false
, it will always be a standalone thread.
A word about TDThread
TDThread
is exposed for architecture purposes and can be created by end users as well, it is not advised and not intended for TDThreads to be created manually.In the very rare case you would have to do so, the execution timing might appear off or the cleanup not occur in a fashion similar to what would be done when enqueuing a TDTask
as Standalone.
Make sure that you started your thread, using TDThread.start()
.
If the TDThread
appears stuck, check for TDThread.is_alive()
and GetStateSafe()
.
If the TDThread
is not alive and is in a state that is not DONE
, you can try to unfreeze the thread by setting the next state.
infoDict = { 'id': str(tdThread.ident), 'name': tdThread.name, 'messageType': managerExt.TDThreadMessageTypes.ON_FRAME_END, 'state': managerExt.TDThreadStates.FINISHING } tdThread.SetStateSafe(managerExt.TDThreadStates.FINISHING) tdThread.InfoQueue.put(infoDict, block=False)
You would likely use SetStateSafe
, and update the InfoQueue
with a valid infoDict
if you wish those messages to be pushed out to subscribers. See section below for details regarding subscribing and subscribers.
If this does not fix the issue, you can always try to call PostProcess
on the TDThread
. Note that doing so might not call the Success
or Except
hooks if they were passed when initializing the TDThread
.
Further customization
They are a few ways to exchange data between a TDThread
and the main thread. Some of which are prepared for you to use.
-
In your threaded method, during execution you can call on the running thread the method
SetProgressSafe
. Passing a float between0.0
and1.0
to represent the current completion of your threaded method.The value will be exposed in the
threadsMonitor
COMP, or you can also access it during each frame on the main thread usingGetProgressSafe
. -
If you want a representation of the current state of a
TDThread
, you can useGetThreadInfoSafe
. This will return adict
. -
If a
TDThread
results in an exception and it wasn’t captured by the user, you can access the exception on theTDThread
object usingGetExceptionSafe
. -
From within threaded method, if you wish to log messages using the
logger
COMP, always useSafeLogger
exposed on thethreadManager
COMP extension.
Subscriber pattern
Similarly to some other components in TouchDesigner, a subscriber pattern is available for use.
A given component can subscribe to the Thread Manager. The component should call the Subscribe
method on the threadManager
COMP if necessary and pass a reference to itself, as well as the message types it needs to subscribe to.
Subscribe(externalComp: str, messageTypes: List[str])
When running a threaded method, a user can add an Info item to the InfoQueue
, similar to the following snippet:
infoDict = { 'id': str(tdThread.ident), 'name': tdThread.name, 'messageType': managerExt.TDThreadMessageTypes.ON_FRAME_END, 'state': managerExt.TDThreadStates.FINISHING } tdThread.InfoQueue.put(infoDict, block=False)
Every frame, the Thread Manager will pull from this queue all pending items.
If a message type is specified that matches a message type a subscriber subscribed to, the infoDict
will be forwarded to the susbcriber, granted that a matching OnMessageReceived
method is implemented on the subscriber.
Conclusion
This is it for a first introduction to the Thread Manager.
We hope that you already have some ideas on how to make use of it in your projects. Please contact us with feedback if you have any requests or suggestions, or perhaps questions.
In the next article we will have a look at integrating a custom Python library into TouchDesigner, relying on the Thread Manager to avoid blocking the main thread during execution as well as the new TouchDesigner Python Environment Manager.