2011-01-22

Threading, locks, and Tkinter's after_idle

I've been getting into Tkinter lately. It's one of the few Python widget toolkits that works on PythonCE. It's actually quite nice. The UIs are not the most friendly, and there are some other things I don't particularly like about Tkinter, but all-in-all, it's been fairly good. (I still like GTK+ better, but it doesn't run on PythonCE, and thus far my applications have all needed to run on my Pocket PC.)

So, I was going along, happily writing and testing, when I hit a slight problem. That slight problem came in the form of an application that just seemed to up and hang for no apparent reason. And when I say hang, I mean the entire Tkinter main loop froze up. What?

I started sprinkling print statements throughout my code to try and find where the freeze-up was occurring, and I eventually found it: it was on an attempt to acquire a lock. Aha! Deadlock! Not the most unusual thing ever. The only problem was, there didn't seem to be any other code that could block after having acquired this lock, and I wasn't using any other locks in my application.

The reason I'm posting about this is because of what ended up being the cause of this. The code that was deadlocking was code bring run on Tkinter's main loop due to a call to tk.after_idle. It turns out that a call to after_idle outside of the event loop itself will block until the currently-running event, if any, finishes. External code was obtaining this lock and then trying to use after_idle. Because the event that would then proceed to attempt to acquire the lock itself was already running, after_idle would block. The event itself would then block.

The solution I'm going to implement is to have a queue of events and a function that runs itself on the event loop 5 or so times every second, processing all the events in the queue. This isn't optimal, but it's the simplest solution I can see at present.

So, the moral of the story is: after_idle doesn't just run its argument when the main loop is idle; it blocks completely until the main loop is idle. So be careful if you're using locks, and when in doubt, use your own queue and a periodic task that runs events in the queue.

I also apologize for sounding like a dope in this post. I'm rather more tired than I usually am for some reason...

No comments: