Login | Register 

Wisdom about python threads?

General discussion about anything TouchDesigner

Wisdom about python threads?

Postby hypnokevin » Thu Jan 25, 2018 8:43 pm

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 :]
Posts: 7
Joined: Wed Jan 17, 2018 12:28 pm

Re: Wisdom about python threads?

Postby elburz » Fri Jan 26, 2018 1:18 am

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.
Elburz Sorkhabi
Creative + Technology
nVoid Art-Tech Limited
Posts: 1937
Joined: Fri Jun 01, 2012 6:55 pm
Location: Toronto, Canada

Re: Wisdom about python threads?

Postby hypnokevin » Sun Jan 28, 2018 12:52 pm

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,
Posts: 7
Joined: Wed Jan 17, 2018 12:28 pm

Re: Wisdom about python threads?

Postby hypnokevin » Sat Feb 17, 2018 7:01 pm

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:

Code: Select all
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:

Code: Select all
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.

Code: Select all
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 :)

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! :)
Posts: 7
Joined: Wed Jan 17, 2018 12:28 pm

Re: Wisdom about python threads?

Postby Ivan » Mon Feb 19, 2018 6:38 am

Great info!
Posts: 100
Joined: Fri Mar 18, 2016 7:12 pm

Re: Wisdom about python threads?

Postby jmt4zj » Thu Mar 01, 2018 4:56 pm

Thanks for posting this info!
User avatar
Posts: 74
Joined: Mon Nov 01, 2010 3:15 am
Location: NYC

Return to General TouchDesigner Discussion

Who is online

Users browsing this forum: Bing [Bot], Google [Bot] and 10 guests