thread(n)

πŸ“¦
version 8.6.17 Β· 2 versions
, in alpine 3.22, alpine 3.23, alpine 3.24, arch rolling, epel 9, fedora 43, fedora 44, opensuse-leap 16.0

thread(n) πŸ’…

okay bestie, gather round, because we're about to get into thread β€” the Tcl extension that gives your scripts access to actual threading. it's giving main character energy and honestly? it ate. this is part 1 of 3, so hydrate and stay with me. πŸ¦‹


NAME

thread β€” Extension for script access to Tcl threading ✨


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?

DESCRIPTION

So the thread extension does the thing: it creates threads that contain Tcl interpreters, and then it lets you yeet scripts over to those threads for evaluation. 🎯

and that's not all β€” it also hands you script-level access to the basic thread synchronization primitives, like mutexes and condition variables. lowkey essential, no cap.


COMMANDS

this section is all about creating threads, destroying threads, and sending scripts off to threads to be evaluated. buckle up. πŸŽ€

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

This one creates a thread that contains a Tcl interpreter. πŸ’–

The Tcl interpreter either evaluates the optional script, if you gave it one, or it just chills in the event loop waiting for scripts that show up via the thread::send command. Either way, both of those things happen simultaneously with the return of the thread::create command back to the caller thread. The caller is not waiting around for the optional script to finish, and the result of that script (if there even is one) does not get returned to the caller. nope. not the vibe.

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 package command out there. This handle goes out of scope automatically when the thread is marked for exit (peep the thread::release command below πŸ‘‡).

If your optional script argument contains the thread::wait command, the thread will enter the event loop. If that command is not found in the script, the thread just runs the script all the way to the end and then exits. In that case you can safely ignore the handle, since it refers to a thread that doesn't even exist anymore by the time the command returns. πŸ’€

Using the flag -joinable lets you create a joinable thread, i.e. one whose exit you can wait on using the thread::join command. ⚠️ heads up: if you fail to join a thread that was created with the -joinable flag, you get resource and memory leaks. so like, don't do that.

Threads created by thread::create cannot be destroyed forcefully. Consequently, there is no corresponding thread destroy command. A thread may only be released using thread::release, and if its internal reference count drops to zero, the thread is marked for exit. That kicks the thread out of event loop servicing, and the thread continues to execute commands passed in the script argument, following the thread::wait command. If that was the last command in the script β€” as is usually the case β€” the thread will exit. ✨

It is possible to create a situation where it may be impossible to terminate the thread. for example, by putting some endless loop after the thread::wait, or by entering the event loop again with a vwait-type of command. In cases like that, the thread may never exit. This is considered bad practice and should be avoided if possible. This is best illustrated by the example below:

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

The thread created in the above example will never be able to exit. 😭 After it has been released with the last matching thread::release call, the thread will jump out of the thread::wait and continue executing the commands that follow. It will enter the vwait command and wait endlessly for events. There is no way one can terminate such a thread, so you wouldn't want to do this! 🚫

Each newly created thread has its internal reference counter set to 0 (zero), i.e. it is unreserved. That counter gets incremented by a call to thread::preserve and decremented by a call to thread::release. These two commands implement a simple but effective thread reservation system, and they offer predictable and controllable thread termination capabilities. πŸ‘‘

It is however possible to create initially preserved threads by using the flag -preserved of the thread::create command. Threads created with this flag have the initial value of the reference counter of 1 (one), and are thus initially marked reserved.

thread::preserve ?id?

This command increments the thread reference counter. Every call to it bumps the reference counter by one (1). The command returns the value of the reference counter after the increment. πŸ”’

If you call it with the optional thread id, the command preserves that given thread. Otherwise, the current thread gets preserved.

With reference counting you can implement controlled access to a shared Tcl thread. By incrementing the reference counter, the caller signalizes that they wish to use the thread for a longer period of time. By decrementing the counter, the caller signalizes that they've finished using the thread. teamwork, bestie. 🀝

thread::release ?-wait? ?id?

This command decrements the thread reference counter. Every call to it drops the reference counter by one (1). If you call it with the optional thread id, the command releases that given thread. Otherwise, the current thread is released. The command returns the value of the reference counter after the decrement. πŸ“‰

When the reference counter reaches zero (0), the target thread is marked for termination. You should not reference the thread after the thread::release command returns zero or a negative integer. The handle of the thread goes out of scope and should not be used anymore. Any following reference to the same thread handle will result in a Tcl error. 😀

The optional flag -wait instructs the caller thread to wait for the target thread to exit, if the effect of the command would 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 this may block the caller thread indefinitely. This option has been implemented for some special uses of the extension and is deprecated for regular use. Regular users should create joinable threads by using the -joinable option of the thread::create command and thread::join to wait for the thread to exit. do it the right way, pls. 🫢

thread::id

This command returns the ID of the current thread. simple. iconic. πŸ’…

thread::errorproc ?procname?

This command sets a handler for errors that occur in scripts sent asynchronously, using the -async flag of the thread::send command, to other threads. If no handler is specified, the current handler is returned. The empty string resets the handler to its default (unspecified) value. πŸͺ„

An uncaught error in a thread causes an error message to be sent to the standard error channel. This default reporting scheme can be changed by registering a procedure which is called to report the error. The procname is called in the interpreter that invoked the thread::errorproc command. The procname is 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 will be used as the error message string; otherwise, a default error message string will be used.

thread::unwind

Use of this command is deprecated in favour of the more advanced thread reservation system implemented with the thread::preserve and thread::release commands. Support for the thread::unwind command 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 will continue 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 the thread::preserve and thread::release commands. Support for the thread::exit command will disappear in some future major release of the extension. πŸ’€

This command forces a thread that's stuck in thread::wait to bail out. Unconditionally. No vote, no warning, it's just gone πŸ’€ The thread's exit status defaults to 666 and you can set your own with the optional status argument. But here's the tea, and it is bad tea: running thread::exit is guaranteed to leave your program memory in an inconsistent state, leak memory, and otherwise mess with other subsystem(s) of your Tcl application in ways nobody can predict. 😭 The command returns an empty result, but in some situations it may trigger a Tcl error with the message "target thread died".


thread::names πŸͺͺ

Hands you back a list of thread IDs. Only the ones made via thread::create though β€” if your app spins up other threads down at the C level, this command does not report them. Sorry bestie, they're invisible to us.

thread::exists id πŸ‘€

Returns true (1) if the thread with the id you passed actually exists, false (0) if it doesn't. Same caveat as above: this only applies to threads created via thread::create.

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 throw in the -async flag, 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 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 names the variable where the result of the script gets stored. Without -async, the command returns the evaluation code, just like the standard Tcl catch command. With -async though, 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
}

So in that example up there, two threads got fed work and both were told to signalize the same variable "result" back in the calling thread. The caller entered the event loop twice to collect both results πŸŽ€ Note, though, that the order you receive those results in may vary, depending on the current system load, the type of work being done, etc, etc.

Many threads can simultaneously send scripts to the target thread for execution. All of them get entered into the target thread's event queue and executed on a FIFO basis, intermingled with any other events pending in that queue. And with the optional -head switch, scripts posted to the thread's event queue can be placed on the head instead of the tail of the queue, which means they run LIFO instead. ✨

thread::broadcast script πŸ“’

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

thread::wait πŸ›‹οΈ

This enters the event loop so a thread can receive messages from the thread::send command. It should only be used inside the script you pass to thread::create, and it should be the very last command in that script. If it isn't, the exiting thread will keep executing the script lines past 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 arguments you pass and evaluates the resulting script under mutex protection. If you don't name a mutex with the optional -lock mutex argument, the internal static mutex gets used.

thread::join id 🀝

Waits for the thread with ID id to exit, then returns its exit code. Errors get returned for threads that aren't joinable or that have already been waited upon by another thread. Once the join happens, that thread's handle has gone out of scope and you should not use it any more. Periodt. πŸ’…

thread::configure id ?option? ?value? ?...? βš™οΈ

Configures various low-level aspects of the thread with ID id, in a similar way to how the standard Tcl command fconfigure configures some Tcl channel options. The options currently supported are -eventmark and -unwindonerror.

  • When -eventmark is 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.
  • When -unwindonerror is 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 🚚

This moves the specified channel from the current thread and interpreter over to 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 now on ✨

The command waits until the other thread has incorporated the channel. And because of that, it's possible to deadlock the participating threads by commanding the other one β€” through a synchronous thread::send β€” to transfer a channel to us. 😭 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. This automatically excludes the special channels for standard input, output and error.

Because of 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 command's 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 the previously detached channel in the current thread/interpreter. For already existing channels, 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 attach, and every subsequent one just does nothing. The command throws an error if the channel cannot be found in the list of detached channels and/or in the current interpreter.

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 all kinds of 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.

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

  • thread::mutex create ?-recursive? β€” Creates the mutex and returns its opaque handle. That handle is what you use 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 there's an 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 may happen in recursive procedure invocations), you should consider using the 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. Obsessed. πŸ‘‘

okay so when a thread just wants to read the resource, it grabs a read lock. read locks are non-exclusive bestie β€” meaning like, more than one thread can hold a read lock on the same resource at the same time, no waiting around for other readers πŸ’…

but the second you wanna change something? that's exclusive write lock territory. a write lock effectively blocks every other thread from getting a read lock while the writer thread is out here modifying the resource. and only after the write lock has been released can the resource be read-locked again. that's the whole vibe check πŸ”₯

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

thread::rwmutex create

creates the reader/writer mutex and hands you back its opaque handle ✨ this handle is what you use for literally any future reference to your brand new mutex, don't lose it πŸŽ€

thread::rwmutex destroy mutex

destroys the reader/writer mutex. but if the mutex is already locked? attempting to destroy it will throw a Tcl error. so don't do that πŸ’€

thread::rwmutex rlock mutex

locks the mutex for reading. more than one thread may read-lock the same mutex at the same time β€” 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. and attempting to write-lock the same mutex twice from the same thread will throw a Tcl error. no double-dipping πŸ™ˆ

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. you can't unlock what's already unlocked, babe 😭


thread::cond

this command gives you script-level access to condition variables. a condition variable creates a safe little 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 attempt to use any other type of mutex in conjunction with the condition variable, a Tcl error will be thrown. not negotiable bestie βœ‹

the command supports these subcommands and options:

thread::cond create

creates the condition variable and returns its opaque handle ✨ this handle is what you use for any future reference to your 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. this one is serious, no cap ⚠️

thread::cond notify cond

wakes up all threads waiting on the condition variable cond. group chat ping, everyone up πŸ“£

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 the cond, the command releases mutex. and before returning to the calling thread, the command re-acquires the mutex again. the unlocking of the mutex and the waiting on the condition variable cond is done atomically ✨

the ms command option, if given, must be an integer specifying the time interval in milliseconds that the command waits to be signalled. otherwise the command waits on condition notify forever πŸ•°οΈ

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 being actually changed β€” spurious thread wake-ups, for example. so yeah, always loop, never trust a single wake-up πŸ’œ


πŸ’¬ 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 β€” the one that created it. periodt πŸ‘‘

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. and for the transfer of bulk information, it's 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. iconic behaviour honestly 🌈


πŸ‘€ SEE ALSO πŸ‘€


πŸ”‘ KEYWORDS πŸ”‘

events, message passing, mutex, synchronization, thread


and that's the whole page bestie, you made it to the end πŸŽ‰ this one's from the Tcl Threading crew, version 2.8, filed under thread(n), and dated 2.8 β€” go forth and synchronize responsibly, mwah πŸ’‹πŸ¦‹