Close
Community Post

Enhancing your Python toolbox with TouchDesigner’s Thread Manager

Threading and TouchDesigner

Threading your Python methods in TouchDesigner can be difficult. While there are approaches available to users to lower the processing cost of a frame caused by specific python utilities (rolling a side process, using asyncio, using the threading library…), most are custom to the project at hand and don’t come with built-in integration.
Noticing the rise of heavier workflows in Python, with machine learning libraries as well as manipulation of large datasets, we decided to develop an internal tool that would be available for us to use as well as expose it to the end user.

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.

A new folder is available in the Palette. Go to ThreadManager and find the threadManagerClient COMP.
You can drag n drop it to your network editor pane.
The settings of the Thread Manager Client are rather minimalist, and that’s the whole point. It’s designed for you to just implement the custom callback and run it.
You can either run manually by clicking the pulse button 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.
Go ahead and click this button, the Thread Manager Client will run the template callback in a thread.
In the Palette, go to 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.

  1. The callback DAT template is fully commented. There is important information that you should read carefully through.
  2. There is prefilled code to demo usage which will run when clicking the Run In Thread pulse.
The 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

While a 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 between 0.0 and 1.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 using GetProgressSafe.

  • If you want a representation of the current state of a TDThread, you can use GetThreadInfoSafe. This will return a dict.

  • If a TDThread results in an exception and it wasn’t captured by the user, you can access the exception on the TDThread object using GetExceptionSafe.

  • From within threaded method, if you wish to log messages using the logger COMP, always use SafeLogger exposed on the threadManager 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.

Comments