Welcome!
This is the community forum for my apps Pythonista and Editorial.
For individual support questions, you can also send an email. If you have a very short question or just want to say hello — I'm @olemoritz on Twitter.
Threading and UI interaction
-
What are the best approaches to having a responsive UI which also allows background threads to run relatively smoothly in Pythonista apps?
More specifically, I'm having trouble in a setup where a user interacts with a custom widget, one where they touch and drag around on it to send messages. I was hoping to have those messages placed into a thread-safe queue and then be processed by a thread running separately from the main/ui threads.
For me, using either the Gestures module, or the touch_began(), touch_moved() and touch_ended() methods of a custom view, the consumer thread which processes the messages very rarely gets a chance to run. Even if I put in time.sleep(0) calls after each time the touch interaction places a message on the queue, trying to force the main thread to release control, I find the background thread seldom gets any time to process...if I pause while interacting, drag around for a moment and then stop, the background thread suddenly gets a chance to process all the queued up messages. But while dragging, almost never.
So, is this just the GIL being it's problematic self? Is there a particular design-pattern which works very well for this scenario, and allows the user to interact relatively smoothly while still letting a background thread perform tasks? Is there some use of in_background(), or the objc_util on_main_thread() which would make this work better?
I'm not married to the threaded system I'm using, if there's a really good asynchronous or semi-asynchronous way of doing things. This is in Python2.7, by the way...so asyncio is not available, and since it's iOS there's no option to use multiprocessing.
-
Are you using a threading Thread? Or in_background, or something else?
All UI callbacks (touch_moved, etc) are executed on the "main" thread-- try to do what you need and return quickly (perhaps calling out to other threads).
The main script is actually executed on a shared background thread, the same one that in_backfround uses. Calls here are queued up, so don't put anything here that is time consuming either, or which blocks, (don't use while True loops in your main script either), otherwise nothing else in the background thread gets to run, since it is shared.threading.Threads should work -- up events are limited to maybe millisec type frequency, and when your finger stops moving or slows down, the events slow down,. Are you doing networking? In that case you might also consider asyncio stuff.
-
Another thing to consider... Are you processing every touch_moved event? Do you need to, or only when motion is above xx pixels?
-
@shinyformica, I am wondering whether you could use
update
to run your workers, and whether that would make any difference in your use case. -
@shinyformica, for reference, the code below works as expected - clock keeps ticking, queue size never grows, and the touch event counter runs smoothly.
#coding: utf-8 from ui import * import time, queue class CustomView(View): def __init__(self, **kwargs): super().__init__(**kwargs) self.label = Label(background_color='white', text_color='black', alignment=ALIGN_CENTER, number_of_lines=0, frame=self.bounds, flex='WH') self.add_subview(self.label) self.update_interval = 1./60. self.queue = queue.Queue() self.counter = 0 self.current_received = 0 def update(self): try: self.current_received = self.queue.get_nowait() except: pass self.label.text = str(round(time.time())) + '\n' + str(self.queue.qsize()) + '\n' + str(self.current_received) def touch_moved(self, touch): self.counter += 1 self.queue.put_nowait(self.counter) if __name__ == '__main__': v = CustomView() v.present()
-
I actually thought I was doing things in a pretty reasonable way, but unfortunately the result I'm seeing isn't smooth threaded running, and I'm not entirely sure why. The code is too complex to post, but here's a schematic of the structure:
Main script:
- Has a main view which uses update() and an update_interval = 0.01
- The update() method of that view pulls incoming data off a thread-safe queue and processes it (processing time is very small)
- There are two standard threading.Threads running: one listens forever on a port for incoming messages via a select.select() call, placing them onto the queue the main view's update() method is reading from, and the other consumes items from another queue for sending out over a socket.
I'm glossing over some details about how interaction with widgets causes messages to end up on the queue for sending, and how incoming messages are processed to update the UI, but those things all operate synchronously, and are very fast.
For the most part the system operates as I expect: threads run, incoming messages show up in the UI via update(), tapping controls which send messages are sent by the sending thread, etc. The problem arises when I have a custom control which needs to send messages continuously while being interacted with. So a user holds down a finger and moves it around and it needs to keep sending out messages until the touch is ended. There I see very jittery performance for the sending thread (sometimes no messages sent, sometimes big chunks of queued-up messages, occasionally smaller chunks). I was expecting that the UI interaction would not "interfere" with the other threads, in the sense that unless it was actively calling draw() or one of the touch*() methods or Gestures methods, it would allow the threads to run...but it seems like during a touch interaction on a custom view, other threads aren't really given a chance to run? I'll try to put together a much simpler example to really test this out.
-
A gist would be helpful...
What is happening during touch_moved?
-
Believe it or not, I don't really know what "gist" is...I'll go look it up.
During touch-moved (or handle_pan() when this is being done via a Gestures callback) on one of these custom controls (though they're all slightly different), it basically:
- updates the control's value by doing some simple calculation
- calls a method which causes a that new value to be wrapped up with a message and queued onto the outbound message queue.
And that's really it. It also generally calls set_needs_display() when the value changes, to make sure the UI reflects the interaction and new value. These things don't take much time at all, but I always see the same thing in both the console output and the "listening" client on a different machine which is receiving these messages: very stuttery output during interaction, rarely receiving a message while the control is being interacted with - though if I pause moving without releasing the touch, suddenly a big chunk of queued-up messages arrive.
Anyway, for the moment I've made things ok for my purposes by having the message send be synchronous, so it happens immediately when the control value changes and isn't queued for send. But I still would love to know if this is something I'm misunderstanding about how the iOS UI+Pythonista UI thread+main thread synchronization works.
One other thing: I tried putting time.sleep(0) in various places during touch interaction and message queueing, to see if I can make sure the other threads are given a chance to run while interacting, but it didn't seem to help...I don't really want to be using events or locks to directly control the timing of thread execution.
-
ahh, sorry, gist is just a simple git share, often just a single file. from the wrench menu, tap share, then tap gist, and you can share your whole code (paste the link) without cluttering the forum. for multiple files, you can tap Edit from the library menu (e.g. this ipad), select which files to share, then tap the share icon at the bottom, and select gist. If you have a github account, you enter your info, thus allowing you to delete the gist later, etc.
-
I don't think time sleeps work in the main thread (anything in the ui) because this is the main thread, and all ui everything stops until your function returns.
it would be helpful to see actual code, but at least describe the structure/functions of how messages are being sent/received, and from what functions and threads.