thread(3tcl)

πŸ“¦ version 2.8.9, in debian bookworm, ubuntu jammy, ubuntu noble

πŸ’– thread β€” the Tcl threading extension, bestie πŸ’–

okay so this is the thread(3tcl) manual page and honestly? it's a whole journey. grab a snack πŸ“, because we're about to learn how to make Tcl spawn threads like it's popping off in the group chat.


✨ NAME ✨

thread β€” the extension that gives your scripts access to Tcl threading. that's it, that's the name, slay.


🌈 SYNOPSIS 🌈

package require Tcl  8.4

package require Thread  ?2.8?

thread::create ?-joinable? ?-preserved? ?script?

thread::preserve ?id?

thread::release ?-wait? ?id?

thread::id

thread::errorproc ?procname?

thread::cancel ?-unwind? id ?result?

thread::unwind

thread::exit ?status?

thread::names

thread::exists id

thread::send ?-async? ?-head? id script ?varname?

thread::broadcast script

thread::wait

thread::eval ?-lock mutex? arg ?arg ...?

thread::join id

thread::configure id ?option? ?value? ?...?

thread::transfer id channel

thread::detach channel

thread::attach channel

thread::mutex

thread::mutex create ?-recursive?

thread::mutex destroy mutex

thread::mutex lock mutex

thread::mutex unlock mutex

thread::rwmutex

thread::rwmutex create

thread::rwmutex destroy mutex

thread::rwmutex rlock mutex

thread::rwmutex wlock mutex

thread::rwmutex unlock mutex

thread::cond

thread::cond create

thread::cond destroy cond

thread::cond notify cond

thread::cond wait cond mutex ?ms?

like, look at all those commands. so many. we're eating good today πŸ’…


πŸ¦‹ DESCRIPTION πŸ¦‹

so the thread extension does two big things, no cap:

  • it creates threads that contain Tcl interpreters, and it lets you send scripts to those threads so they get evaluated over there
  • it also gives you script-level access to the basic thread synchronization primitives β€” think mutexes and condition variables, the whole squad

πŸ‘‘ COMMANDS πŸ‘‘

this whole section is about making threads, destroying threads, and yeeting scripts into threads to get evaluated. main character energy fr fr.

🌟 thread::create ?-joinable? ?-preserved? ?script?

this one creates a thread, and inside that thread lives a whole Tcl interpreter. iconic.

the interpreter does one of two things:

  • if you passed the optional script, it evaluates that script
  • otherwise it just chills in the event loop waiting for scripts that show up via thread::send

and here's the thing β€” both of those happen at the same time as thread::create returning to the caller thread. the caller is not waiting for the script to finish, and the result of the script (if it even has one) is not coming back to the caller. not me expecting a return value 😭 there isn't one.

the result of thread::create is the ID of the thread. that's the opaque handle that identifies your shiny new thread for every other command in the package. the handle goes out of scope automatically once the thread is marked for exit (peep thread::release below for that whole saga).

if the optional script argument contains the thread::wait command, the thread will hop into the event loop. if thread::wait is not in there, the thread runs the script to the end and then just... exits. in that case you can safely ignore the handle, because it's pointing at a thread that doesn't exist anymore by the time the command returns. gone. ghosted. πŸ’€

using the flag -joinable lets you create a joinable thread β€” meaning somebody can wait on its exit using the thread::join command. and listen: failure to join a thread created with -joinable results in resource and memory leaks. that's the warning, take it seriously bestie 🫢

threads made by thread::create cannot be destroyed forcefully. so there's no thread destroy command, periodt. a thread can only be released using thread::release, and if its internal reference count drops to zero, the thread gets marked for exit. that kicks the thread out of event loop servicing, and then the thread keeps on executing commands passed in the script argument, right after the thread::wait command. if that was the last command in the script (which is usually the case), the thread exits. bye boo πŸ‘‹

now, you can accidentally create a situation where it's literally impossible to terminate the thread. how? put some endless loop after the thread::wait, or enter the event loop again with a vwait-type of command. in those cases the thread may never exit. this is considered bad practice and you should avoid it if you can. here's the cautionary tale:

# You should never do ...
set tid [thread::create {
    package require Http
    thread::wait
    vwait forever ; # <-- this!
}]

the thread created above will never be able to exit. after it's been released with the last matching thread::release call, it jumps out of thread::wait and keeps executing the commands that follow. it hits vwait and waits endlessly for events. there is no way to terminate a thread like that, so you would not want to do this. do not do this. πŸ™ˆ

every newly created thread has its internal reference counter set to 0 (zero) β€” i.e. it's unreserved. that counter gets bumped up by a call to thread::preserve and bumped down by a call to thread::release. these two commands together implement a simple but genuinely effective thread reservation system, and they give you predictable, controllable thread termination. πŸ’–

that said, you can make threads that start out preserved, using the -preserved flag of thread::create. threads created with this flag start with a reference counter value of 1 (one), so they're marked reserved from the jump.

🌟 thread::preserve ?id?

this command increments the thread reference counter. every single call bumps it by one (1). the command returns the value of the reference counter after the increment.

if you pass the optional thread id, it preserves that given thread. if you don't, it preserves the current thread.

with reference counting you can implement controlled access to a shared Tcl thread. incrementing the counter is basically the caller saying "hey, I wanna use this thread for a while longer." decrementing it is the caller saying "okay I'm done with this thread now." simple as that. ✨

🌟 thread::release ?-wait? ?id?

this one decrements the thread reference counter. every call knocks it down by one (1).

if you pass the optional thread id, it releases that given thread. if you don't, the current thread gets released. the command returns the value of the reference counter after the decrement.

when the reference counter hits zero (0), the target thread is marked for termination. do not reference the thread after thread::release returns zero or a negative integer. the handle goes out of scope and should not be used anymore. any following reference to that same thread handle will result in a Tcl error. πŸ’€

the optional flag -wait tells the caller thread to wait for the target thread to exit β€” but only if the effect of the command would actually result in termination of the target thread, i.e. if the return result would be zero (0). without the flag, the caller thread does not wait for the target thread to exit.

care must be taken when using -wait, since it may block the caller thread indefinitely. this option was implemented for some special uses of the extension and is deprecated for regular use. regular users should create joinable threads using the -joinable option of thread::create and then use thread::join to wait for the thread to exit. πŸ‘‘

🌟 thread::id

returns the ID of the current thread. short, sweet, iconic.

🌟 thread::errorproc ?procname?

this sets a handler for errors that happen in scripts sent asynchronously (that's the -async flag of thread::send) to other threads.

if no handler is specified, the current handler is returned. the empty string resets the handler back to the default (unspecified) value.

by default, an uncaught error in a thread causes an error message to be sent to the standard error channel. you can change that default reporting scheme by registering a procedure that gets called to report the error. procname gets called in the interpreter that invoked the thread::errorproc command. and it's called like this:

myerrorproc thread_id errorInfo

🌟 thread::cancel ?-unwind? id ?result?

this command requires Tcl version 8.6 or higher.

it cancels the script being evaluated in the thread given by the id parameter.

  • without the -unwind switch: the evaluation stack for the interpreter is unwound until an enclosing catch command is found, or until there are no further invocations of the interpreter left on the call stack
  • with the -unwind switch: the evaluation stack for the interpreter is unwound without regard to any intervening catch command, until there are no further invocations of the interpreter left on the call stack

if result is present, it gets used as the error message string. otherwise a default error message string is used.

🌟 thread::unwind

use of this command is deprecated in favour of the more advanced thread reservation system implemented with thread::preserve and thread::release. support for thread::unwind will disappear in some future major release of the extension. ⚠️

this command stops a prior thread::wait command. execution of the script passed to the newly created thread continues from the thread::wait command. if thread::wait was the last command in the script, the thread will exit.

the command returns an empty result, but it may trigger a Tcl error with the message target thread died in some situations. 😭

🌟 thread::exit ?status?

use of this command is deprecated in favour of the more advanced thread reservation system implemented with thread::preserve and thread::release. support for thread::exit will disappear in some future major release of the extension. ⚠️

...and that's where we're pausing, because there's more coming. stay tuned bestie πŸŽ€

This command yanks a thread that's stuck in thread::wait out of it, no questions asked, it just leaves. πŸ’€ The thread's exit status defaults to 666 and you can override that with the optional status argument. Okay but bestie, listen β€” running thread::exit is guaranteed to leave your program memory in an inconsistent state, produce memory leaks, and mess with other subsystem(s) of your Tcl application in ways nobody can predict. It returns an empty result, but in some situations it may trigger a Tcl error saying "target thread died". So like… iconic but unhinged. πŸ™ˆ

thread::names

Returns a list of thread IDs. And it's only for threads that were created via thread::create, okay? If your app makes other threads down at the C level, this command simply does not know about them. Not its business. πŸ’…

thread::exists id

Returns true (1) if the thread given by the id parameter exists, and false (0) if it doesn't. Again β€” only for threads made with thread::create. That's the whole guest list. πŸŽ€

thread::send ?-async? ?-head? id script ?varname?

This one passes a script over to another thread and, if you want, waits around for the result. ✨

If you pass -async, it does not wait β€” it just returns an empty string and dips. The target thread has to enter its event loop to receive scripts sent this way. That happens automatically by default for threads created without a startup script. Threads can also enter the event loop explicitly by calling thread::wait or any other relevant Tcl/Tk command, like update, vwait, etc.

The optional varname is the name of the variable where you want the result of the script stored. Without -async, the command returns the evaluation code, just like the standard Tcl catch command. But if -async is specified, the command returns immediately and the caller can later vwait on ?varname? to get the result of the script it passed:

set t1 [thread::create]
set t2 [thread::create]
thread::send -async $t1 "set a 1" result
thread::send -async $t2 "set b 2" result
for {set i 0} {$i < 2} {incr i} {
    vwait result
}

In the example above, two threads got fed work and both were told to signalize the same variable "result" in the calling thread. The caller entered the event loop twice to collect both results. But heads up: the order the results come back in may vary, depending on the current system load, the type of work being done, etc, etc. It's giving chaos, but it's documented chaos. 😌

Many threads can send scripts to the target thread at the same time. They all get entered into the target thread's event queue and executed on a FIFO basis, intermingled with any other events pending in that queue. Using the optional -head switch, scripts posted to the thread's event queue can be placed on the head instead of the tail β€” so they run LIFO style. Queue jumper energy. πŸ‘‘

thread::broadcast script

Passes a script to all the threads created by the package, for execution. It does not wait for a response from any of them. Fire and forget, fr fr. πŸ”₯

thread::wait

This enters the event loop so a thread can receive messages from thread::send. It should only be used inside the script you passed to thread::create, and it needs to be the very last command in that script. If it isn't, the exiting thread will keep executing the script lines past the thread::wait, which is usually not what you want and/or expect. 😭

set t1 [thread::create {
    #
    # Do some initialization work here
    #
    thread::wait ; # Enter the event loop
}]

thread::eval ?-lock mutex? arg ?arg ...?

Concatenates the passed arguments and evaluates the resulting script under mutex protection. If you don't hand it a mutex with the optional -lock mutex argument, it uses the internal static mutex. Locked and loaded. πŸ”’

thread::join id

Waits for the thread with ID id to exit, then returns its exit code. You'll get errors for threads that aren't joinable or that have already been waited on by another thread. Once you join, the handle of the thread has gone out of scope and should not be used any more. Gone, bye, don't text it. πŸ‘‹

thread::configure id ?option? ?value? ?...?

Configures various low-level aspects of the thread with ID id, the same way the standard Tcl command fconfigure configures Tcl channel options. Options currently supported: -eventmark and -unwindonerror.

  • -eventmark β€” when given a value greater than 0 (zero), that value is the maximum number of asynchronously posted scripts that may be pending for the thread. thread::send -async blocks until the number of pending scripts in the event loop drops below the -eventmark value.
  • -unwindonerror β€” when given a value of true, an error result in a script causes the thread to unwind, making it unavailable to evaluate additional scripts.

thread::transfer id channel

Moves the specified channel out of the current thread and interpreter and into the main interpreter of the thread with the given id. After the move, the current interpreter has no access to the channel any more, but the main interpreter of the target thread will be able to use it from then on. The command waits until the other thread has incorporated the channel.

Because of that wait, it is possible to deadlock the participating threads by commanding the other one through a synchronous thread::send to transfer a channel to us. And that easily extends into longer loops of threads all waiting on each other. πŸ˜΅β€πŸ’«

Other restrictions: the channel in question must not be shared among multiple interpreters running in the sending thread. That automatically excludes the special channels for standard input, output and error.

Due to the internal Tcl core implementation and the restriction on transferring shared channels, you have to take extra measures when transferring socket channels created by accepting the connection out of the socket commands callback procedures:

socket -server _Accept 2200
proc _Accept {s ipaddr port} {
    after idle [list Accept $s $ipaddr port]
}
proc Accept {s ipaddr port} {
    set tid [thread::create]
    thread::transfer $tid $s
}

thread::detach channel

Detaches the specified channel from the current thread and interpreter. After that, the current interpreter has no access to the channel any more. The channel sits in a parked state until some other (or the same) thread attaches it again with thread::attach. Restrictions: same as for transferring shared channels with the thread::transfer command. πŸ…ΏοΈ

thread::attach channel

Attaches a previously detached channel in the current thread/interpreter. For channels that already exist, the command does nothing β€” i.e. it is not an error to attach the same channel more than once. The first operation actually performs the operation, while all the subsequent operations just do nothing. The command throws an error if the channel can't be found in the list of detached channels and/or in the current interpreter. Lowkey forgiving, highkey strict. πŸ¦‹

thread::mutex

Mutexes are the most common thread synchronization primitives. They're used to synchronize access from two or more threads to one or more shared resources. This command gives you script-level access to exclusive and/or recursive mutexes. Exclusive mutexes can be locked only once by one thread, while recursive mutexes can be locked many times by the same thread. For recursive mutexes, the number of lock and unlock operations must match β€” otherwise the mutex will never be released, which leads to various deadlock situations. 😬

Care has to be taken when using mutexes in a multithreading program. Improper use of mutexes may lead to various deadlock situations, especially when using exclusive mutexes. This is the part where you read twice. πŸ’€

The thread::mutex command supports the following subcommands and options:

thread::mutex create ?-recursive?

Creates the mutex and returns its opaque handle. This handle should be used for any future reference to the newly created mutex. If no optional -recursive argument was specified, the command creates the exclusive mutex. With the -recursive argument, the command creates a recursive mutex. ✨

thread::mutex destroy mutex

Destroys the mutex. The mutex should be in an unlocked state before the destroy attempt. If the mutex is locked, the command will throw a Tcl error.

thread::mutex lock mutex

Locks the mutex. Locking the exclusive mutex may throw a Tcl error if you attempt to lock the same mutex twice from the same thread. If your program logic forces you to lock the same mutex twice or more from the same thread (this can happen in recursive procedure invocations), you should consider using recursive mutexes instead. πŸ‘‘

thread::mutex unlock mutex

Unlocks the mutex so some other thread may lock it again. Attempting to unlock an already unlocked mutex will throw a Tcl error.

thread::rwmutex

This command creates many-readers/single-writer mutexes. Reader/writer mutexes let you serialize access to a shared resource more optimally. In situations where a shared resource gets mostly read and seldom modified, you might gain some performance by using reader/writer mutexes instead of exclusive or recursive mutexes. 🌈

Okay so for reading the resource, a thread should grab a read lock on it. A read lock is non-exclusive β€” meaning more than one thread can read-lock the same resource at the same time, no waiting on other readers. But for changing the resource? A thread must get an exclusive write lock. That lock effectively blocks all threads from gaining the read-lock while the resource is being modified by the writer thread. Only after the write lock is released can the resource be read-locked again. periodt.

The thread::rwmutex command supports these subcommands and options:

  • thread::rwmutex create β€” Creates the reader/writer mutex and returns its opaque handle. Keep that handle, bestie β€” you'll use it for any future reference to the newly created mutex. πŸ’…
  • thread::rwmutex destroy mutex β€” Destroys the reader/writer mutex. If the mutex is already locked, trying to destroy it will throw a Tcl error. so don't do that unless you want chaos 😭
  • thread::rwmutex rlock mutex β€” Locks the mutex for reading. More than one thread may read-lock the same mutex at the same time. slay, sharing is caring ✨
  • thread::rwmutex wlock mutex β€” Locks the mutex for writing. Only one thread may write-lock the same mutex at the same time. Attempting to write-lock the same mutex twice from the same thread will throw a Tcl error. no double-dipping, bestie πŸ™ˆ
  • thread::rwmutex unlock mutex β€” Unlocks the mutex so some other thread may lock it again. Attempting to unlock an already unlocked mutex will throw a Tcl error. so keep track, okay? πŸ’€

πŸ¦‹ thread::cond πŸ¦‹

This command gives you script-level access to condition variables. A condition variable creates a safe environment for your program to test some condition, sleep on it when it's false, and get awakened when it might have become true. A condition variable is always used in conjunction with an exclusive mutex. If you try to use any other type of mutex with the condition variable, a Tcl error will be thrown. no cap 🚫

The command supports these subcommands and options:

  • thread::cond create β€” Creates the condition variable and returns its opaque handle. Use this handle for any future reference to the newly created condition variable. πŸŽ€
  • thread::cond destroy cond β€” Destroys condition variable cond. Extreme care has to be taken that nobody is using (i.e. waiting on) the condition variable β€” otherwise unexpected errors may happen. like, be so careful 😭
  • thread::cond notify cond β€” Wakes up all threads waiting on the condition variable cond. wakey wakey besties β˜€οΈ
  • thread::cond wait cond mutex ?ms? β€” This command suspends program execution until the condition variable cond has been signalled or the optional timer has expired. The exclusive mutex must be locked by the calling thread on entrance to this command. If the mutex is not locked, a Tcl error is thrown. While waiting on cond, the command releases mutex. Before returning to the calling thread, the command re-acquires the mutex again. Unlocking the mutex and waiting on the condition variable cond is done atomically. iconic behavior, honestly πŸ’œ

The ms command option, if given, must be an integer specifying a time interval in milliseconds the command waits to be signalled. Otherwise, the command waits on condition notify forever. forever ever? forever ever? ⏳

In multithreading programs, there are many situations where a thread has to wait for some event to happen until it is allowed to proceed. This is usually accomplished by repeatedly testing a condition under the mutex protection and waiting on the condition variable until the condition evaluates to true:

set mutex [thread::mutex create]
set cond  [thread::cond  create]

thread::mutex lock $mutex
while {<some_condition_is_true>} {
    thread::cond wait $cond $mutex
}
# Do some work under mutex protection
thread::mutex unlock $mutex

Repeated testing of the condition is needed since the condition variable may get signalled without the condition actually being changed (spurious thread wake-ups, for example). so always loop, bestie πŸ”

πŸ’¬ DISCUSSION πŸ’¬

The fundamental threading model in Tcl is that there can be one or more Tcl interpreters per thread, but each Tcl interpreter should only be used by a single thread which created it. A "shared memory" abstraction is awkward to provide in Tcl because Tcl makes assumptions about variable and data ownership. Therefore this extension supports a simple form of threading where the main thread can manage several background, or "worker" threads. For example, an event-driven server can pass requests to worker threads, and then await responses from worker threads or new client requests. Everything goes through the common Tcl event loop, so message passing between threads works naturally with event-driven I/O, vwait on variables, and so forth. For the transfer of bulk information it is possible to move channels between the threads.

For advanced multithreading scripts, script-level access to two basic synchronization primitives, mutex and condition variables, is also supported. we love a flexible queen πŸ‘‘

πŸ‘€ SEE ALSO πŸ‘€

πŸ”‘ KEYWORDS πŸ”‘

  • events
  • message passing
  • mutex
  • synchronization
  • thread

This page is from Tcl Threading, version 2.8, section thread(3tcl). Stay synchronized, bestie β€” you got this πŸ’–βœ¨

πŸ“¦ where this page comes from