Tkinter sometimes has a busy-wait main loop (more or less)

March 26, 2010

One of my always-running programs is a little Tkinter-based Python app. For a while now I've noticed that it was accumulating a surprising amount of CPU time and would sometimes show up on top as active, despite me not doing anything with it. Today I finally got around to figuring out why, and the answer is that Tkinter's main loop effectively busy-waits in some Python environments.

The standard way for Tkinter-based programs to operate is to make all of the Tkinter calls to set up your windows and then call root.mainloop() (where root is the object you got from calling Tk()). If your version of CPython is built with thread support (the common case) and your version of Tcl is built without threads enabled (this depends), the inner core of mainloop() looks more or less like this:

while keep_running:
   Tcl_DoOneEvent(TCL_DONT_WAIT);
   if no event processed:
      sleep(20 msec);
   check_for_signals();

(One reason for this loop is to handle Unix signals promptly; the full code also has some thread-related locking stuff. See Tkapp_MainLoop() in Modules/_tkinter.c in the CPython source for the gory details.)

The net effect is that when your Tkinter-based program is sitting idle, it wakes up every 20 milliseconds to spin around doing nothing; over time this can add up to visible and even significant CPU usage.

(In theory the sleep interval can be increased; in practice you can't turn this up without lowering the responsiveness of your application, because your program won't process new events until it wakes up from the sleep (and it's going to wind up in the sleep fairly often). If you really want to touch this, see Tkinter._tkinter.setbusywaitinterval().)

My solution was to replace my use of root.mainloop() with the following code:

global exit_mainloop
while not exit_mainloop:
    root.dooneevent(0)

In Tkinter-related code where I was previously calling .quit() on Tkinter objects to get the application to quit, I instead set the exit_mainloop global to 1; this is more or less what .quit() does anyways. This is probably somewhat less efficient if your application is active all the time (since you now go through (more) interpreted Python code for every event), but is much more efficient if your application spends most of its time idle; strace now shows my program sitting there doing nothing instead of constantly twitching around in system calls.

The one caution with this approach is that .mainloop() also exits if there are no Tk main windows left. If this matters for your application, you'll need to keep track of this yourself and set exit_mainloop appropriately.

Sidebar: how to see if your Tcl is built with threads enabled

Run tclsh and give it the command 'parray tcl_platform'. If it has a threaded entry, your platform built Tcl with --enable-threads (this information is from here). It appears that Fedora 11 and FreeBSD build Tcl without threads while Debian, Ubuntu and RHEL 5 build it with threads.

(I have no idea why Fedora and RHEL are different here.)

Written on 26 March 2010.
« The problem with overly verbose package installation
Testing in the face of popen() »

Page tools: View Source, Add Comment.
Search:
Login: Password:
Atom Syndication: Recent Comments.

Last modified: Fri Mar 26 01:39:37 2010
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.