thread(3tcl)
π 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-joinableoption ofthread::createand then usethread::jointo 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
-unwindswitch: the evaluation stack for the interpreter is unwound until an enclosingcatchcommand is found, or until there are no further invocations of the interpreter left on the call stack - with the
-unwindswitch: the evaluation stack for the interpreter is unwound without regard to any interveningcatchcommand, 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 withthread::preserveandthread::release. support forthread::unwindwill 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 withthread::preserveandthread::release. support forthread::exitwill 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 than0(zero), that value is the maximum number of asynchronously posted scripts that may be pending for the thread.thread::send -asyncblocks until the number of pending scripts in the event loop drops below the-eventmarkvalue.-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 variablecond. 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 variablecond. wakey wakey besties βοΈthread::cond wait cond mutex ?ms?β This command suspends program execution until the condition variablecondhas been signalled or the optional timer has expired. The exclusivemutexmust be locked by the calling thread on entrance to this command. If the mutex is not locked, a Tcl error is thrown. While waiting oncond, the command releasesmutex. Before returning to the calling thread, the command re-acquires themutexagain. Unlocking the mutex and waiting on the condition variablecondis 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 π
http://www.tcl.tk/doc/howto/thread_model.htmltpooltsvttrace
π KEYWORDS π
eventsmessage passingmutexsynchronizationthread
This page is from Tcl Threading, version 2.8, section thread(3tcl). Stay synchronized, bestie β you got this πβ¨
π¦ where this page comes from
- debian bookworm: tcl-thread 1:2.8.8-2
- ubuntu jammy: tcl-thread 1:2.8.7-1
- ubuntu noble: tcl-thread 1:2.8.9-1build1
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms