Updated 2017-06-15 19:58:43 by RJM

Keeping a GUI alive during a long calculation examines options for keeping a GUI up to date while doing something that is expected to take a long time.

See Also  edit

Countdown program
Colliding Balls
The moveBalls procedure is a fairly simple example of one technique.
Firework Display
Another example that does not use update, and in which a procedure, explode, calls itself again after a delay.
Scripted wrappers for legacy applications, Part 2
A different treatment of some of the same material

Description  edit

KBK 2001-04-19:

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 open. You use 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.

WGRM 2004-08-26:

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?

AF: You can get the exit status of a pipe from the global variable, $errorCode.

The format used when a child process has exited with a non-zero exit status CHILDSTATUS pid code

AMG 2005-11-02: I describe the child process method on Threads vs. events.

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 sim::init and that it does a small amount of work by calling sim::one_step. sim::one_step 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: update 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.

davidw: What could go wrong in this specific example?

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 (PYK 2012-09: the docs no-longer contain this warning, but it still applies):
     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]
      #this would work just as well:
      #after 0 [list after idle 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.

Peter da Silva: It seems to me that this operation should be explicitly supported by the event loop. This is precisely the kind of obscure yak shaving that makes Tcl hard to get up to speed on.

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

Code  edit

# 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 sim::init:
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 sim::one_step is called. This procedure returns 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
}

# update_crosshairs 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
}

# doOneStep 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 eq "doOneStep"
            || $script eq "after 0 doOneStep" } {
            after cancel $e
        }
    }
}

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

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

TV: The idea of the title of this page is of course important: often, when the computation model resembles Model / View / Controller, the maker of a computer program with GUI expects the computer it runs on to have sufficient horsepower to complete a requested operation in interaction time, and that other programs aren't in the way to prevent this.

For computation tasks / computer power combinations where waiting is to be expected, probably indeed one wants the UI to allow interactions in the meanwhile, but this would probably also require another UI/application interaction pattern definition.

Assuming that a database is a separate process, IPC seems reasonable enough to deal with that particular case, and then read in return data in the normal event loop, which is safer and usually more programming-decent then threading, though milage and opinions may vary.

AMG 2005-03-18: First let's see if I understand this. If not, correct me and disregard everything else I say. :^)

It's not safe to recursively [update], but if you desire the same effect you must schedule your continuation to be run after all pending idle tasks and timed tasks have completed.

This is very, very much like yielding to the OS and letting it reschedule your task. (I sorely wish we had [yield] instead of [update] to schedule the current continuation, jump to the event loop, then jump back when it is fair to do so.)

I'm looking at the documentation for sqlite's eval and progress (sub)commands [1]. [$db eval] allows you to specify code to run for every row returned. [$db progress] schedules a callback to be executed after doing however-many sqlite virtual machine opcodes. How do I use these facilities to keep my GUI alive? Reading between the lines, I sense that I am expected to supply [update] as the callback paramter to [$db progress].

In other news, I'm pondering the new API for my itunesdb, which is still slow (takes more than one second) for large music collections. I'd like for it to avoid blocking the GUI for more than a heartbeat, especially since the display system expects the application to redraw itself on expose events, and "painting" looks very ugly. So how do I accomplish this? When reading playlists, a single record can be very large (especially the master playlist, which contains all songs), therefore returning to the event loop in between each read isn't good enough.

Peter Newman 2005-03-19: I don't understand why reading a record from a file should be slow - but assuming it is:-

  • Perhaps you could write a Tcl program to translate the itunesDB file to one from which individual records can be retrieved quickly, or;
  • Run the GUI, and the itunesDB file parser, as two separate processes. You want the file parser to load the records into memory, for example. So that the GUI can quickly retrieve records, using some form of inter-process communication.
  • If keeping the GUI alive is the only issue (and not the speed with which it's drawn), the simplest and safest way is - assuming the GUI comprises whatever it comprises - and a listbox or table that you want to fill with data from the itunesDB file:-
proc myFantasticItunesApp { } {
    #...draw the GUI - with the listbox(s)/table EMPTY...
    after idle AddOneRecordToTheListbox/Table
    #--OR--
    after 10-or-some-other-small-number-of-milliseconds AddOneRecordToTheListbox/Table
    }

with:-
 proc AddOneRecordToTheListbox/Table { } {
         #...add one or some other small number of records to the listbox - however many you can get without blocking for too long - say half a second max...
        after idle AddOneRecordToTheListbox/Table
        #--OR--
        after 10-or-some-other-small-number-of-milliseconds AddOneRecordToTheListbox/Table
        }

Your GUI should be as responsive as possible - even if reading an individual record is so slow that they just dribble into the listbox. Also this method is basically the same as using update - but, unlike update, it's perfectly safe. P.S. NEVER, EVER use update (unless someone is quite literally holding a gun to your head).

AMG 2005-03-18 (I suspect we may be in different timezones!): Sorry, I based the slow-record-reading comment on the performance of itunesdb 0.01, which was quite atrocious. Back then, reading the master playlist (with 4137 tracks) of my sample iTunesDB file took 8.5 seconds (400 MHz machine). Now, with 0.03 on the same machine, it's down to 1.8 seconds, or 0.45 seconds on a 1500 MHz. That's not so bad after all. Plus I have further speed optimizations in mind. Yum.

Here's a proc that does however many units of work it can in some period (say, 100 ms), then updates a status widget, reschedules itself with [after], and returns to the event loop. It works by measuring the time to do the first unit of work then divides that from the update period to get the number of work units it can do per period. It also tracks the actual time spent doing that many work units, and if it's off by more than 50% (or some amount) it remembers to recalibrate the next time it runs.
package require Tk

set work(total) 10000 ;# Total number of times to call do_work.
set work(done)      0 ;# Number of times do_work has been called.
set work(period)  100 ;# Time in milliseconds between display updates.
set work(steps)     0 ;# Number of times to call do_work between updates.
set work(recalib)   1 ;# If true, recalibrate before next update.
set work(start)     0 ;# Start time of calculation.

proc do_work {} {
    for {set foo 0} {$foo < 1000} {incr foo} {}
}

proc next {} {
    global work

    if {$work(recalib)} {
        set start [clock microseconds]
        do_work; incr work(done)
        set dur [expr {[clock microseconds] - $start}]

        set work(steps) [expr {int(1e3 * $work(period) / $dur)}]
        if {$work(steps) == 0} {
            set work(steps) 1
        }

        .steps-v configure -text [format %d $work(steps)]
    }

    if {$work(total) - $work(done) < $work(steps)} {
        set work(steps) [expr {$work(total) - $work(done)}]
    }

    set start [clock milliseconds]

    for {set step 0} {$step < $work(steps)} {incr step} {
        do_work; incr work(done)
    } 
       
    set dur [expr {[clock milliseconds] - $start}]
       
    .percent-v configure -text [format %.2f [expr {
            100.0 * $work(done) / $work(total)}]]
    .actual-v configure -text [format "%d ms" $dur]
    .elapsed-v configure -text [format "%d ms" [expr {
            $dur + $start - $work(start)}]]

    if {abs($dur - $work(period)) > $work(period) / 2} {
        set work(recalib) 1
    }
   
    if {$work(done) < $work(total)} {
        after idle [list after 0 next]
    }
}

proc main {} {
    foreach {name text value} [list                                         \
        percent "Percent complete:" 0.00     steps  "Steps per period:" ?   \
        target  "Target period:"    ?        actual "Actual period:"    ?   \
        elapsed "Time elapsed:"     "0 ms"
    ] {
        label .$name-l -text $text
        label .$name-v -text $value
        grid .$name-l .$name-v
        grid configure .$name-l -sticky w
        grid configure .$name-v -sticky e
    }
    .target-v configure -text [format "%d ms" $::work(period)]

    set ::work(start) [clock milliseconds]
    next
}

main

I wrote a Window Maker dockapp (in Tcl/Tk!!) that lets me switch between 400 MHz and 1500 MHz (terrific for benchmarking). Anyway, if I click it while running the above code, the "steps per period" value automatically changes to keep the period more-or-less constant.

Peter Newman 2005-03-19: I'd say that's about the best you can do using standard Tcl (and without going to more exotic solutions like threads or separate processes - and without speeding up the read routines somehow). Is the GUI now responsive enough (it should be - once you've tuned it right) - both at 400 MHz and 1500 MHz?

AMG: Thanks, it's very nice. There's not much concern for responsiveness, since the only thing worth clicking is Cancel.

AMG: Here's an example using Tcl 8.6 coroutines.
package require Tcl 8.6
package require Tk
proc quixx {win} {
    global traillen smooth
    for {set i 0} {$i < 8} {incr i} {
        lappend points [list [expr {rand() * 320}] [expr {rand() * 240}]\
                             [expr {rand() * 2 - 1}] [expr {rand() * 2 - 1}]]
    }
    set trail {}
    set angle 0
    set pi [expr {acos(-1)}]
    while {1} {
        set vertices {}
        for {set i 0} {$i < [llength $points]} {incr i} {
            lassign [lindex $points $i] x y dx dy
            set x [expr {$x + $dx}]
            if {$x <= 0 || $x >= 320} {
                set dx [expr {-$dx}]
            }
            set y [expr {$y + $dy}]
            if {$y <= 0 || $y >= 240} {
                set dy [expr {-$dy}]
            }
            lappend vertices $x $y
            lset points $i [list $x $y $dx $dy]
        }
        lappend vertices [lindex $vertices 0] [lindex $vertices 1]
        while {[llength $trail] > $traillen} {
            $win delete [lindex $trail 0]
            set trail [lrange $trail 1 end]
        }
        set color [format #%02x%02x%02x\
                [expr {int(cos($angle) * 127 + 127)}]\
                [expr {int(cos($angle + $pi * 2 / 3) * 127 + 127)}]\
                [expr {int(cos($angle + $pi * 4 / 3) * 127 + 127)}]]
        set angle [expr {fmod($angle + $pi / 100, 2 * $pi)}]
        lappend trail [$win create line $vertices -fill $color -width 2\
                       -smooth $smooth]
        after 0 [list after idle [info coroutine]]
        yield
    }
}
set smooth 0
canvas .c -width 320 -height 240 -highlightthickness 0 -background black
scale .trail -from 1 -to 200 -variable traillen -orient horizontal
radiobutton .poly -text Polygonal -variable smooth -value 0
radiobutton .curvy -text Curvy -variable smooth -value 1
pack .c .trail -fill x
pack .poly .curvy -side left -expand 1
wm resizable . 0 0
coroutine resume quixx .c

This code makes a pretty animation as fast as it can. To limit the frame rate, replace the "after ..." line with "after $frametime [info coroutine]". As was described previously on this page, combining [after 0] and [after idle] is the way to tell the parent event loop to do something immediately after handling everything that's already enqueued.

Of course, the parent event loop can't do anything until execution returns to it. Returning is a pain, since it doesn't just interrupt your code, it terminates it. You have to write your code to be able to pick up where it left off.

That's where coroutines come in. If your code yields instead of returns, it can be resumed, not merely restarted. Happily, the [info coroutine] command returns the name of the command to call to resume the current coroutine.

The classical return,-restart,-and-pick-up-the-pieces approach gets especially difficult when there's a lot of state data, because the state data can't be stored in local variables if it must persist between invocations. Coroutines make it possible to keep this state data in local variables, greatly simplifying your code.

Let's say you have a procedure that does a long-running operation, and now you want to add a GUI which must be kept alive. Before Tcl 8.6, you had to rewrite your procedure in complex ways described elsewhere on this page, but with coroutines you can simply do "after 0 [list after idle [info coroutine]]; yield" every once in a while. Then use the [coroutine] command to start your procedure. (The first argument to [coroutine] is totally arbitrary, since it will be retrieved by [info coroutine].) Simple!

Using coroutines in this manner is basically a coöperative multithreading system, with the event loop playing the role of scheduler. The advantage over preëmptive multithreading is the implicit synchronization and locking: all you have to do is to avoid calling [yield] when shared resources are in an inconsistent state. For example, if I had made $points a global variable, I wouldn't call [yield] halfway through changing it.

The challenge is figuring out how often to yield. Yielding at every iteration of a tight inner loop can make your program unacceptably slow. You might find inspiration in my above post from 2005, which gives a non-coroutine example of dynamically determining the amount of work between yields in order to maintain a target GUI response time. It continuously adapts to the timing characteristics of the work being performed, which I tested and confirmed by changing the CPU clock rate in the middle of execution.

Update: I made [quixx] take a parameter naming the canvas widget. This makes it possible to run multiple instances of quixx concurrently. Just add code similar to the following:
canvas .c2 -width 320 -height 240 -highlightthickness 0 -background black
pack .c2 -fill x
coroutine resume2 quixx .c2

This simple approach wouldn't be possible if [quixx] used [update], since only once [quixx] could run at a time.

Having to manually name each coroutine instance kind of sucks, though. The name's not important, since [quixx] uses [info coroutine].

RLE 2011-02-12 17:10 EST: And you (sillymonkeysoftware) deleted the entire contents of the old page, replacing it with only your question. I've restored the previous page contents above. Please be more careful in the future:

[sillymonkeysoftware] - 2011-02-12 16:42:49

Hi Folks,

I'm having a bit of difficulty implementing the simple GUI keep-alive as described above. My program needs to calculate and export large chunks of audio data. If I do it in a while-loop I get a frozen gui and spinning beach ball (Mac) until everything completes. Really not what I want the users to suffer through, and there's no way to halt it once it's begun.

I was thrilled when I found the examples above. So here's what I initially tried, but it seems to only export one iteration of the loop, then returns:
proc export_loop {} {
    set ::gExportStatus [continue_export]
    if {$::gExportStatus > 0} {
        after idle [list after 0 export_loop]
    }
    return
}

export_loop

So, after numerous attempts to get the above working, I'm now using this:
set ::gExportStatus 1

while {$::gExportStatus > 0} {
    update idletasks
    after idle [dict get $gInterface progress_bar] configure -value $::gExportStatus
    set ::gExportStatus [continue_export]
}

which seems, well... hideous. and is only slightly better than the spinning beach ball frozen gui. There's still no way to actually catch a halt from a button press and stop the export.

Now, continue_export is actually a C function that returns:

  • The number of frames that were exported.
  • A zero if all frames have exported and there is nothing left to do.
  • A negative number if there was an error during export.

Is the problem that I'm throwing the call to the C function in the middle of the proc? Any thoughts?

Ug! I am truly sorry for overwriting the old page!!!!! Won't happen again!

AMG: Does [continue_export] return immediately, or does it take a long time to complete? Until [continue_export] returns, your entire program is blocked. Everything you said suggests to me that [continue_export] is holding up your GUI. Consider making it work in smaller chunks, or move it to another thread or process.

[sillymonkeysoftware]: [continue_export] doesn't export all the audio at once, it breaks each song up into blocks of 0x20000 samples, and my C code is structured to keep track of how many blocks have been completed, and which ones still remain. One pass of [continue_export] completes in ~1 second.

I just can't seem to find any logical reason why the "after idle [list after 0 export_loop]" in my first example only completes one iteration, and stops. It should recursively call itself until gExportStatus < 0 I've been trying to avoid threads, because it will add a lot more complexity to an already complex program... but, I may need to go the threads route anyway unless I can figure this out.

AMG: One SECOND!? There's your problem. :^) If you can make each pass of [continue_export] finish in, say, 0.1 seconds, you might have a chance. Also, I suggest printing $gExportStatus to see if it's working the way you expect.

RJM Some comments above suggest that much of long calculation stuff can be programmed (e.g. database access via pipes) without sacrificing GUI performance. Nevertheless, there are cases where this is not possible. Example: I'm creating a specific image viewer where a lot of images are pre-loaded via the img-package while having the opportunity to operate on images that have become visible. A "step by step"-approach as suggested above is not viable here, since each image could take a second or so to load (and resized via xphoto.dll -> Enhanced photo image copy command. Browsing the already loaded part would not work as expected...

The only solution is to use threads. It was my very first thread programming in Tcl. As a simple aid in checking the fluentness of a GUI while programming whatever CPU-consuming stuff (in Threads or as small step-style procecurese etc.), I created a small visual aid in this:

The code can be sourced at will, and its "radar beam" turns 6 degrees counterclockwise each 40 ms cycle (25 Hz). Code:
toplevel .fm
wm attributes .fm -toolwindow 1 -topmost 1

canvas .fm.c -width 41 -height 41 -bg #ffff80
pack .fm.c
.fm.c create arc 2 2 42 42 -start 5 -extent 24 -fill brown -outline brown -tag arc
proc fmon {angle} {
    set angle [expr {$angle + 6.0}]
    if {$angle >= 360.0} {set angle 0.0}
    if {[winfo exist .fm.c]} {
        after 40 fmon $angle  ;# 25 Hz rate
        .fm.c itemconfig arc -start $angle
    }
}
fmon 0.0
.fm.c create oval 2 2 42 42

In order to ease Thread programming, I came up with a background helper function which let an asynchronous call to a function defined within a thread::create procedure behave just like an ordinary proc call:
proc bgproc {script} {
    thread::send -async $::tid $script _result
    vwait _result
    return $::_result
}

Of course, $tid could be an argument in bgproc, but I decided to not do it. Reminding Ousterhout's Why Threads are a Bad Idea, one should strive to not use more than one thread. In fact this is eased by the nice thread eventloop functionality trough thread::wait together with thread::send.

In the image use case, the background functionality (in the thread::create body) does this:
# within proc imgload
    image create photo im_orig -file $file
    im_scaled copy im_orig -filter Mitchell -scale $factor -rotate $angle
    im_scaled data -format {jpeg -quality 88}  ;# returned string retured to GUI

Actually, the resized image is transferred to the GUI via e.g.
$im2 put [bgproc [list imgload $file]

... which is part of a foreach loop that does GUI-update image by image.

My first thread experiences in Tcl makes me quite happy about its simplicity and versatility.