Tcl does not share (normal) variables between threads. Instead, you need to work by sending messages between threads. A message is just a (usually short) script that you ask the other thread to run (the result of the script can be handled in a few ways, including a synchronous wait or by running the script disconnected). Most of the time, you set up a procedure in the receiving thread to actually do the work.
Let's restructure your waiting thread to operate that way:
set waiter [thread::create {
proc do {filename targetThread returnMessage} {
set delay [expr {int(1000* 60* 60* 2)}]
while true {
# This would be really a do-while loop, but we don't have those
while true {
set old_log_size [file size $filename]
after $delay
set new_log_size [file size $filename]
if {$old_log_size == $new_log_size} break
}
puts "I suspect Test is in hung state... checking again after 2 hours...\n"
after $delay
set new_log_size [file size $filename]
if {$old_log_size == $new_log_size} break
}
puts "\n\n Test is in hung state... log has not updated since last 4 hours...\n\n"
# Send message to main thread to do something about the hung test
thread::send -async $targetThread $returnMessage
}
thread::wait
}]
We'd set that thread actually working like this:
thread::send -async $waiter [list do $root/logfile [thread::current] {set test_hung 1}]
However, the only long operations in there are the calls to after. (Well, unless you're fantastically unlucky with the OS calls to get the log file size.) That means we can convert to using an asynchronous form in the thread, leaving the thread open to being accessed while it is working.
set waiter [thread::create {
proc do {filename targetThread returnMessage} {
set delay [expr {int(1000* 60* 60* 2)}]
set old_log_size [file size $filename]
# Schedule the run of do2 in two hours
after $delay [list do2 $filename $targetThread $returnMessage $delay $filename $old_log_size]
}
proc do2 {filename targetThread returnMessage delay filename old_log_size} {
set new_log_size [file size $filename]
if {$old_log_size == $new_log_size} {
puts "I suspect Test is in hung state... checking again after 2 hours...\n"
# Schedule the run of do3 in another two hours
after $delay [list do3 $filename $targetThread $returnMessage $delay $filename $old_log_size]
} else {
# An update did happen; run ourselves again in two hours to compare to the new size
after $delay [list do2 $filename $targetThread $returnMessage $delay $filename $new_log_size]
}
}
proc do3 {filename targetThread returnMessage delay filename old_log_size} {
set new_log_size [file size $filename]
if {$old_log_size == $new_log_size} {
puts "\n\n Test is in hung state... log has not updated since last 4 hours...\n\n"
# Send message to main thread to do something about the hung test
thread::send -async $targetThread $returnMessage
} else {
# An update did happen; run ourselves again in two hours to compare to the new size
after $delay [list do2 $filename $targetThread $returnMessage $delay $filename $new_log_size]
}
}
thread::wait
}]
So… we've got manageability but lost readability (the API for use is identical). Not bad but not great! (This sort of restructuring is known as conversion to Continuation-Passing Form, and it tends to destroy code readablity.) In 8.6 we can do better because we have coroutines that can yield to the thread's event loop.
set waiter [thread::create {
proc do {filename targetThread returnMessage} {
coroutine Coro[incr ::Coro] doBody $filename $targetThread $returnMessage
}
proc delayForTwoHours {} {
set delay [expr {int(1000* 60* 60* 2)}]
after $delay [info coroutine]
yield
}
proc doBody {filename targetThread returnMessage} {
while true {
while true {
set old_log_size [file size $filename]
delayForTwoHours
set new_log_size [file size $filename]
if {$old_log_size == $new_log_size} break
}
puts "I suspect Test is in hung state... checking again after 2 hours...\n"
delayForTwoHours
set new_log_size [file size $filename]
if {$old_log_size == $new_log_size} break
}
puts "\n\n Test is in hung state... log has not updated since last 4 hours...\n\n"
# Send message to main thread to do something about the hung test
thread::send -async $targetThread $returnMessage
}
thread::wait
}]
That (which still has the same API calling convention) gives manageability yet keeps virtually all the code (especially apart from the short bits in their own procedures) looking the same as the first version I wrote. Under the covers, the coroutine does the rewriting to continuation-passing form, but that's now handled by the Tcl runtime instead of needing to be done explicitly in your code. (Also, Tcl uses explicit coroutine launching, but that in turn means that it can yield across multiple stack levels without the complex yield chains of some other languages.)
I leave it as an exercise to use the second or third version as the basis for a version of the code which doesn't need extra threads at all. Running processes in the background also doesn't need threads; this whole management process can work with just a single (user-visible) thread.
[thread::send]in one thread and[vwait]in another; here's how.