Version 10 of Keep a GUI alive during a long calculation

Updated 2004-08-26 21:57:39

PURPOSE: Examine options for keeping a GUI up to date while doing something that is expected to take a long time. (also see Countdown program)


KBK - 19 April 2001

The single thread model that the Tcl event loop uses makes for simpler programming in most cases, because the Tcl programmer doesn't generally need to worry about thread safety. Most programmers with experience in dealing with threaded applications are happy to avoid the insidious bugs that result when the complex and subtle rules for threads are violated.

Unfortunately, one place where threads really shine is where two components of an application compete for resources. Perhaps the commonest example that new Tcl programmers call to mind arises in dealing with long-running calculations. If the GUI is to be kept alive, then it must get some CPU resources, even if the long-running calculation doesn't relinquish them willingly.

The easiest way to structure this sort of thing is to use another process, which you launch with the open command. You use a fileevent on the resulting pipe to monitor the process's output and do whatever you need to do in the GUI, and your user interface stays alive.


BM - 26 August 2004 That might be a problem when you want the exit code. However you can get around that on Linux using a compound command that echoes the exit code. Another possibility, surely, is to use send -async?


Sometimes, though, using another process isn't feasible (you may, for instance, need to be portable to Macintosh) or you have other reasons for wanting to keep your calculations in the same address space as your GUI. In this case, your best bet is to structure your code so that it does a little bit of work at a time.

Let's work through some examples. The first example plots trajectories of a cannonball at various elevation angles of the cannon. Full code for the example is shown at the bottom of this page. A second, much simpler example, follows.

Starting with the cannonball example, let's just note that the calculation begins by calling the [sim::init] procedure and that it does a small amount of work by calling [sim::one_step]. The [sim::one_step] procedure returns 1 if there is more work to be done and 0 if it's finished.

We'll be plotting the results on a canvas, and we want to keep a set of cross-hairs on the canvas up to date at all times:

  proc update_crosshairs { w x y } {
    $w coords xhair $x 0 $x [winfo height $w]
    $w coords yhair 0 $y [winfo width $w] $y
    $w raise xhair
    $w raise yhair
  }

  grid [canvas .c -width 640 -height 480]
  .c create line 0 0 0 480 -tags xhair -fill white
  .c create line 0 0 640 0 -tags yhair -fill white
  bind .c <Motion> {update_crosshairs %W %x %y}

A naive Tcl programmer would then want to implement the long-running calculation using [update]:

    sim::init .c 640 480
    while { [::sim::one_step] } {
        update
    }

This approach has a number of pitfalls, which are discussed in Update considered harmful and Tcl event loop. The primary problem is unexpected recursion: the [update] command enters the event loop recursively, and it in turn may enter event handlers recursively. If the event handlers aren't expecting to be re-entrant, chaos will result.


What could go wrong in this specific example? -davidw


A far better practice is to return to the event loop between steps, using [after idle] to schedule the next calculation once the events settle down. Alas, simply doing this isn't safe! An 'after idle' that reschedules itself causes trouble, as the manual warns:

     At present it is not safe for an idle callback to reschedule itself
     continuously.  This will interact badly with certain features of
     Tk that attempt to wait for all idle callbacks to complete.
     If you would like for an idle callback to reschedule itself
     continuously, it is better to use a timer handler with a zero
     timeout period.

Even this warning is oversimplified. Simply scheduling a timer handler with a zero timeout period can mean that the event loop will never be idle, keeping other idle callbacks from firing. The truly safe approach combines both:

  proc doOneStep {} {
    if { [::sim::one_step] } {
        after idle [list after 0 doOneStep]
    }
    return
  }
  sim::init .c 640 480
  doOneStep

This skeleton should be considered the basic framework for performing long running calculations within a single Tcl interpreter.


For those who want to try this out, I include the complete program below.

# We'll create a namespace that holds the state of the calculation at a given time:

  namespace eval sim {

    variable dt 0.2;                    # Integration step size
    variable v 98;                      # Muzzle velocity (m/s)
    variable g 9.8;                     # Acceleration of gravity

    variable stepNum;                   # Number of steps performed so far
    variable maxSteps;                  # Maximum number of steps needed

    variable canvas;                    # Path name of the canvas that
                                        # displays results
    variable xorig;                     # X coordinate of the cannon on canvas
    variable yorig;                     # Y coordinate of the cannon on canvas
    variable scale;                     # Scale factor (pixels per meter)

  }

# The plotting can be initialized with a call to the 'sim::init' procedure:

  proc sim::init { w width height } {

    variable stepNum
    variable maxSteps

    variable dt
    variable v
    variable g

    variable canvas
    variable xorig
    variable yorig
    variable scale

    set stepNum 0
    set maxSteps 101

    set vy [expr { sqrt( 0.5 ) * $v }]
    set t [expr { $vy / $g }]
    set x [expr { 2 * $vy * $t }]

    set canvas $w
    set xorig [expr { $width / 2 }]
    set yorig [expr { $height * 0.95 }]

    set scale [expr { 0.45 * $width / $x }]

    return

  }

# A single trajectory will be plotted each time the 'sim::one_step' procedure is called. This procedure will return 1 if there is still work to be done, or 0 if the last trajectory has been calculated.

  proc sim::one_step {} {

    variable stepNum
    variable maxSteps

    variable dt
    variable v
    variable g

    variable canvas
    variable xorig
    variable yorig
    variable scale

    if { $stepNum >= $maxSteps } {
        return 0
    }

    # Run one simulation run

    set angle [expr { 3.14159 * $stepNum / ( $maxSteps - 1 ) }]
    set vx [expr { $v * cos($angle) }]
    set vy [expr { $v * sin($angle) }]

    set x 0.0
    set y 0.0

    set coords {}

    while { $y >= 0.0 } {

        set cx [expr {  $scale * $x + $xorig }]
        set cy [expr { -$scale * $y + $yorig }]
        lappend coords $cx $cy
        set x [expr { $x + $vx * $dt }]
        set y [expr { $y + $vy * $dt }]
        set vy [expr { $vy - $g * $dt }]

    }

    eval [list $canvas create line] $coords

    incr stepNum

    return 1
  }

# The 'update_crosshairs' procedure keeps the cross-hairs on the canvas aligned with the mouse pointer.

 proc update_crosshairs { w x y } {
    $w coords xhair $x 0 $x [winfo height $w]
    $w coords yhair 0 $y [winfo width $w] $y
    $w raise xhair
    $w raise yhair
 }

# The 'doOneStep' procedure is discussed above.

 proc doOneStep {} {
    if { [::sim::one_step] } {
        after idle [list after 0 doOneStep]
    }
    return
 }

# The main program creates the canvas and the cross-hairs, and establishes bindings to keep the cross-hairs up to date.

 grid [canvas .c -width 640 -height 480]
 .c create line 0 0 0 480 -tags xhair -fill white
 .c create line 0 0 640 0 -tags yhair -fill white
 bind .c <Motion> {update_crosshairs %W %x %y}

# There are two alternative ways to manage switching between handling GUI events and doing a piece of calculation. The second one is preferred.

 # set naive 1; # to show the behavior of the naive implementation
 if { [info exists naive] } {
    sim::init .c 640 480
    while { [::sim::one_step] } {
        update
    }
 } else {
    sim::init .c 640 480
    doOneStep
 }

Simplified Example: Stopping a Long Calculation

This very simple example illustrates the same principle as the cannonball example, but with much less code. The GUI is simply a label and start and stop buttons. A global variable stores the value displayed by the label. The "long running" calculation just increments the variable.

    # very simple GUI for long running calculation
    grid [label .l -textvariable count]
    grid [button .b1 -text start -command start]
    grid [button .b2 -text stop -command stop]
    set count 0

The calculation is performed one step at a time, as recommended in the earlier example. One approach uses a global variable as a flag which is checked as the calculation progresses.

    proc start { } {
        set ::running 1
        doOneStep
    }

    proc stop {} {
        set ::running 0
    }

    proc doOneStep {} {    
        if { $::running } {
            incr ::count
            after idle [list after 0 doOneStep]
        }
        return
    }

Another approach would be to stop the calculation by intervening directly into the event queue. At the time the stop routine is called, one of the two scripts "doOneStep" or "after 0 doOneStep" is in the queue. Cancelling the event stops the calculation.

    proc start { } {
        doOneStep
    }

    proc stop {} {
        foreach e [after info] {
            set script [lindex [after info $e] 0]
            if { $script == "doOneStep" || \
                    $script == "after 0 doOneStep"} {
                after cancel $e
            }
        }
    }

    proc doOneStep {} {    
        incr ::count
        after idle "after 0 doOneStep"
    }




A different treatment of some of the same material appears in "Scripted wrappers for legacy applications, Part 2" [L1 ].


[We should write up a threaded example, at least for completeness. Threading *does* remain relevant for lots of database work, for instance.]


Category GUI