Get the sample code here: PyObjC-Thread.zip
This is a small sample project showing how to invoke Python methods as background tasks when writing applications using the PyObjC Python to Objective-C bridge. Normally any method invoked as the result of a User Interface action is run in the UI thread, which for most fast tasks is just fine, there will be no noticable degredation of user experience.
But when a long running task is run on the primary application thread, the UI will become non-responsive, and updates to the UI from within the task will be delayed until the task completes, which means that any indications of progress will be lost. This can be seen in the sample application with the startProgressNoThread_ and runProgressNoThread methods:
@objc.IBAction
def startProgressNoThread_(self, sender):
"""
This method is triggered by the "Progress without Thread" button in
the UI.
"""
NSLog("Starting the progress bar without background thread")
self.runProgressNoThread()
def runProgressNoThread(self):
"""
Update an NSProgressIndicator in a loop, with a small delay between
each update to the indicator.
Because this method is called on the application event loop, nothing
will update on the UI until this method is complete. The NSProgressIndicator
will not properly update, and the calls to disable and then enable the
buttons will not happen properly. All of the UI updates will be delayed
until this method has finished. This method also completely locks the UI,
allowing no interaction or input.
"""
self.buttonsEnabled(False)
self.progress_bar.setMinValue_(0.0)
self.progress_bar.setMaxValue_(100.0)
self.progress_bar.setIndeterminate_(False)
for i in range(0, 101):
self.progress_bar.setDoubleValue_(i * 1.0)
time.sleep(0.05)
self.buttonsEnabled(True)
These methods can be found in the PyObjC_ThreadAppDelegate.py file. The startProgressNoThread_ method is triggered by the Progress without Thread button in the application interface. If you run this application, and click on this button, you’ll notice that the interface locks up for the duration of the runProgressNoThread method, with no UI updates and no UI response. You can’t even exit the program normally. To fix this user experience we can take advantage of the NSThread Objective-C class, and run this task in a background thread. Adding a background thread introduces only two new lines of code, and modifies one existing line of code as compared to the non-threaded example.
To invoke a background thread we need to create an NSThread object and initialize it with a method to execute in the background, and then start the thread:
@objc.IBAction
def startProgressThreaded_(self, sender):
"""
This method is triggered by the "Progress with Thread" button in the
UI. Instead of directly calling a Python method, we use the NSThread
class to start a new background thread, passing the object method to
be invoked in this new thread.
"""
NSLog("Starting progress bar with background thread")
t = NSThread.alloc().initWithTarget_selector_object_(self,self.runProgressThreaded, None)
t.start()
This method is triggered by the Progress with Thread button in our interface. Rather than directly calling our task (as the startProgressNoThread_ method does), we create an NSThread, with the method runProgressThreaded as an argument. The t.start() call kicks off our background thread.
Every thread in an Objective-C program uses a pool of memory to manage and control how and when memory is allocated to objects, and how that memory gets cleaned up. The application thread has already created the memory pool used by the UI and the rest of our program, but because we’re starting out own background thread, we need to make sure to create a new NSAutoreleasePool to be used by objects within our thread.
def runProgressThreaded(self): """ Update an NSProgressIndicator in a loop, with a small delay between each update to the indicator. This method is called in a background thread. Each thread in an Objective-C program has it's own NSAutoreleasePool for memory management. We need to create this pool before updating any objects in our UI. Creating and then releasing this autorelease pool only adds two lines of code, as compared to the runProgressNoThread method. While this method is running the UI will update normally and respond to user input. """ self.buttonsEnabled(False) pool = NSAutoreleasePool.alloc().init() self.progress_bar.setMinValue_(0.0) self.progress_bar.setMaxValue_(100.0) self.progress_bar.setIndeterminate_(False) for i in range(0, 101): self.progress_bar.setDoubleValue_(i * 1.0) time.sleep(0.05) pool.release() self.buttonsEnabled(True)
The runProgressThreaded method is nearly identical to the runProgressNoThread method, with the addition of the memory pool, which we create using the NSAutoreleasePool class. If you run the application you’ll see that when you click the Progress with Thread button the UI remains responsive, the NSProgressIndicator updates throughout the task process, and you can exit the program normally while the task is running.
This example is obviously contrived, but running a task in the background like this is feasible for any long running Python method.