Wisdom about python threads?

Greetings! I’m new to TouchDesigner but I’m an experienced programmer. I’ve started using a technique for long operations in python and I’d like to know if it seems reasonable in a TD context, or if there are better or more standard ways of doing things.

For this application, I have to communicate with three HTTP APIs (including sending files) and use a number of command line programs (a slew of small utilities to control a Fastec camera, and ffmpeg to assemble the camera’s output frames into an h264 video file). My general approach so far has been to kick off work in a threading.Thread, then use a Timer CHOP to poll for a result. I use a threading.Lock for memory safety.

For the HTTP stuff, this seems to work fine. The use case is pretty simple – make a sequence of requests in python, and at the end, make sure things turn out OK. I’m using the requests module and write “synchronous” code that then wrapped in a thread.

I’m doing something similar with the command-line tools, but I use the subprocess module. The ffmpeg code should be very similar to the http/requests code. The camera controls are a bit more complex, so I’m using a queue.Queue (which is conveniently synchronized) to pass command and response objects. A Timer CHOP calls a poll method that pops things off the queue and reacts accordingly.

How does this sound to experienced TouchDesigner users? I think I’m not doing too much work in the threads, mostly waiting, so consuming the GIL shouldn’t be a problem. I do worry about error handling, especially as a TD newbie, but I’m trying to fix that with my schedule – freeze features early and test for a couple weeks. And ask for help early!!

Are there other common approaches to performing long tasks in TD without impacting the frame rate? Could I be doing more, or less, with python vs TD native operators?

I’ve considered wrapping the functionality of the camera’s command-line tools in a DLL with the CPlusPlus op – is this advisable over using pipes in python?

Many thanks in advance for any help with my vague questions :]

Using threads can be dangerous but the danger is mostly in regards to interacting with TouchDesigner objects from different threads. If you use queues to communicate with a thread, then you should be ok. You can check this component we made to see an example of using queues:


Alternatives that may also work and be easier:

  1. Run another process of TouchDesigner that handles tasks that will drop frames, that way your main render process runs smoothly, and the other process can still use a lot of the nice features in TouchDesigner and simple visual processing, but can also launch Python commands that might drop a bunch of frames. They can communicate via Touch In/Out operators or OSC or UDP. Benefit being you can stay in TouchDesigner more.

  2. Run a separate Python command line app that is a TCP/UDP server with all your extra functionality built into it, then send network commands to run the requests/ffmpeg commands. You can then communicate back to TouchDesigner with TCP/UDP and send JSON packets if you are sending back lots of data. The benefit to this is you can work fully inside of a regular Python environment, and dropping frames here won’t effect your render process, and it should reduce the complexity of your threading and queues.

Thanks for that! It’s good to hear that my threads-and-queues approach has been done by more experienced TD people world. I think talking over a local socket is a good idea, too. Unless you have to send a lot of data, it’s certainly achievable within the 16ms frame budget, and it separates concerns and makes the code more reusable and testable. I do think I’d use a language other than Python to gain access to real threads and lose the GIL, though!

Using a separate TD process is something I may do after I’m a bit more experienced with it ;]

Thanks again,

I figured I’d post an update in case anybody finds this thread in the future.

I have a big TD file that makes heavy use of HTTP networking with python’s “requests” module. I’ve found a nice workflow: I write simple synchronous code that uses “requests”, which blocks TD rendering. Then I wrap it in a python thread. It’s really simple for “fire-and-forget” requests:

from threading import Thread
import requests

def upload_file(path):
    Thread(target=upload_file_sync, args=(path,)).start()

def upload_file_sync(path):
    with open(path, 'rb') as file:
        requests.post(URL, data=file)

I often need to chain a sequence of requests, so I do something like this:

def do_upload_sequence():

def do_upload_sequence_sync():
    provisioned = provision()
    if not provisioned:
        print("provisioning failed")

    uploaded = upload()
    if not uploaded:
        print("uploading failed")

    confirmed = confirm_upload()
    if not confirmed :
        print("confirm failed")

def provision():
    # requests code...
    return resp.ok

def upload():
    # as above
    return resp.ok

def confirm_upload():
    # requests code...
    return resp.ok

If I need to do something with the result of a network operation, I add a Timer CHOP with a short length. Every time it fires, it polls for the result. In python, I use two global variables, one for the result, and one for a threading.Lock. I always make sure to acquire the lock for the shortest time possible. When the request starts, I start the timer and then start a thread to do the work; the timer polls until a result arrives, which stops the timer.

from threading import Thread, Lock

TEXT = None
TEXT_LOCK = Lock()

def poll():
    text = None
    with TEXT_LOCK:
        text = TEXT
    if text is not None:
        op('text').text = text

def network_op():

def network_op_sync():
    global TEXT
    resp = requests.post(...)
    if resp.ok:
        with TEXT_LOCK:
            TEXT = resp.text

Python note: there is no need to use “global TEXT” as the first line of poll() as I’m only reading its value. The function that assigns to TEXT must declare “global TEXT” to reassign it from None to a string. There is no need to use “global TEXT_LOCK” because there is only one instance of Lock being referenced. Also, because of the GIL, there may be no need for a mutex lock, but I include it to be safe and signal my intention. (Stylistically, my use of ALL_CAPS for non-constant globals is questionable…)

Another issue I dealt with is error control – what happens when a critical network operation fails? For this, I added some logic to my “poll” Timer CHOPs. I set a maximum length of time as a timeout value, and if this was exceeded I called a “reset” script to get my TD network back to normal. Canceling threads isn’t possible in python, so I add a global CANCEL bool (and CANCEL_LOCK) to any scripts that have sequences of network calls (like the upload sequence above), and check it between each network call.

The only time I had issues with python affecting my frame rate is when I tried to use OpenCV to load an image from disk (cv2.imread(“path”)). Even inside a python thread, this blocked TD for a noticeable number of frames. Luckily, I was able to implement my image processing with TD TOPs and CHOPs instead :slight_smile:

One random note – I had no luck setting “Python 64-bit module path” in Preferences. Instead, I have one script that adds all my modules to sys.path. I couldn’t figure out how to make TD evaluate it first, so instead I use an Execute DAT to fire a short timer in onStart() and update all the other scripts that import third-party modules. (I often use Script CHOPs because I can easily create a collection of buttons with appendPulse(), so I do op(‘scriptchop’).par.setuppars.pulse() to make TD re-evaluate the script once sys.path is properly set up.)

Hopefully this helps somebody in the future! :slight_smile:

Great info!

Thanks for posting this info!

I can no longer strongly recommend this technique. It can lead to race conditions (crashes) within TD that shouldn’t happen, but do. That said, I have a few installations that have lasted for about a year without crashing that do use python threads for networking. In these cases, making threads is relatively rare (it only happens if a user triggers a certain condition), and I decided that it’s worth the risk of crashing instead of rewriting everything. But, for cases where I was using threads to poll a server every couple seconds, I no longer do this. I was getting crashes every few days with this technique.

The workaround is to use the Web DAT. If your use case is as simple as a single GET or POST, you can easily get away with this. If you have more complex needs, I recommend proxying a Web DAT request to a local web server using something simple like python’s Flask. Let the web server do your work and report status back to the Web DAT.

This increases complexity a bit – now you have to make sure Touch and your little server are both always running – but you won’t be scratching your head wondering why TD is crashing.

Ha, I asked for wisdom about python threads in TD, and the wise elburz recommended a proxy setup like this. Maybe I should have listened!

:laughing: I think I’m still jsut as mixed as you about it. If it’s super simple, I may use threading, but I think like you’ve experienced, the head scratching almost isn’t worth the effort in most cases, and rolling a quick side Python cli tool that does all that stuff and then pinging it from either Web DAT or in most of the cases I’ve done through TCP/IP sending JSON packets. With the Python app acting as TCP server, and your TouchDesigner project being the client, you can reliably get API streaming JSON data in as well. Kind of annoying like you said to make sure both are working, but I find the Python cli stuff ends up being so minimal and simple that it’s farrrrrrrrrrrrrrrr more likely TouchDesigner won’t be working and needs monitoring than my cli script.

Threading is definitely not my area of expertise, but I’m interested in making this kind of stuff easier. For myself as well as others!

Can someone summarize the features a threading system for Touch would support? And maybe give a simple example of a problem that would require it?