mkThreads 1.2 - Multithreading for Tcl/Tk 8.1 and higher.
Introduction
Commands
Notes
Examples
Installation
Changes
Author
mkThreads provides commands that allow for development of multithreaded applications under Tcl/Tk. Threads can be created that either asyncronuously execute a script or wait for scripts sent by other threads (including, of course, the main thread). Mutexes can be defined and threads can wait on so-called Conditions to synchronize with other threads. In addition, a method is implemented to define variables which are common for all threads (The thread concept of Tcl includes that each thread must have an own interpreter. Therefore, not even global variables of one thread are known to any other thread).
mkThreads 1.2 incorporates the new thread API that comes with Tcl 8.3.1, but still compiles with the old-style API for Tcl 8.1 through Tcl 8.3.0. It is stubs enabled for versions 8.3.1 and higher.
It is recommended to read all thread relevant information provided at Scriptics' web site. See also the "threads" man page that describes the concept of mutexes and conditions.
The mkThreads package adds four new commands: thread, mutex, tvar and serialize. They accept several options and arguments as follows:
Creates a new thread and returns the thread's identifier. If Script is specified then the new thread will execute it and then terminate. If Script is not specified then the thread will wait on the event loop to receive scripts from other threads by means of "thread eval". Script itself can also put the thread into the event loop by e.g. calling vwait at the end. If the option -init is specified, then init.tcl will be run (by means of calling Tcl_Init()).
Sends a script to a thread. The thread is specified by its ID as returned from "thread create". If the option -wait is specified then the Script is executed while the sending thread is waiting on the result. The result of the Script, though executed in another thread, would then become the result of this call, including any error information in errorCode and errorInfo. If -wait is not specified, Script will be executed asynchronuously, and the call will return immediately. If, in addition, a Command is specified right after Script, then it will be executed when Script has finished. This Command will be called with at least two arguments: The first argument is a result code (0: TCL_OK, 1: TCL_ERROR, 2: TCL_RETURN), the second argument the Script's result. If Script exited with an exception, then two more arguments will be appended, errorCode and errorInfo. The sending thread must be in the event loop, e.g. by means of vwait. Command and -wait are mutual exclusive options.
Exits the current thread, i.e. the thread that invokes this command. If this thread is the main thread, then the application is terminated just as with the exit command.
Defines or returns a Tcl command, which is called when a thread exits. Command will be called with the exiting thread's ID, therefore it should be a procedure that takes one argument. The thread that executes Command must be in the event loop, e.g. by means of vwait. Only one thread can "own" the exit procedure. If another thread has previously defined an exit procedure, then it will be overwritten. If Command is not specified, a two-element list is returned. The first element is the thread that will execute Command, the second element is the Command itself.
Defines or returns a list of all packages that are to be auto-loaded for each newly created thread. If PackageList is specified, it's elements must be the names of currently present packages. The mkThreads package itself does not need to be included in PackageList, its commands are already part of a new thread. Note that "package require" won't work in threads unless you specify the -init option with "thread create".
Defines or returns a list of all procedures set to be auto-loaded for each newly created thread. If ProcList is specified, it's elements must be the names of existing procedures. When a new thread is created, these procedures will be created in the thread's associated interpreter. This feature allows for a more "linear" code just as with e.g. C/C++, since procedures don't have to be redefined. Note that these procedures are physical copies of their originals. That is, if one of these procedures is overwritten later in the program, the other threads will not know about this.
Returns the thread identifier of the current thread, i.e. the thread that invokes this command.
Returns 1 if the thread specified by ThreadId exists, 0 otherwise.
Returns a list with all currently existing threads, including the main thread.
Registers a channel specified by ChannelId for the thread specified by ThreadId. This is a very silly thing to do, since Tcl is not really ready for that. Please consider it as an experimental feature. Conceptually this is similar to the "interp transfer" command. Read the BUGS section below for important restrictions before you use this command!
Unregisters the channel specified by ChannelId. If ThreadId is specified, it unregisters the channel in that thread, otherwise it will unregister it in the current thread. Only those channels that have been registered by means of "thread _register_" must be applied. As for "thread _register_", this is purely experimental.
Creates a mutex and returns its name. If Name is specified, it will be used as the mutex' name, otherwise a string in the from mutex1 is returned.
Deletes the mutex specified by Name. This will fail if any threads are still trying to acquire that mutex.
Locks the mutex specified by Name. If this mutex has already been locked by another thread, then this thread will wait until it is unlocked by the other thread. If the same thread tries to lock a mutex twice, this command will return with an error.
Unlocks the mutex specified by Name. If the thread is not the owner of that mutex, i.e. hasn't locked it before, then this command will return with an error.
Waits for another thread to notify that mutex. If no timeout in form of milliseconds is specified, the thread will potentially wait forever. Otherwise it will wait the given amount of time only. The command will return 1 if a timeout occured and 0 otherwise. The mutex must be held in order to wait for notification (see the threads man page for detailed explanations).
Notifies any threads that are waiting for that mutex. It is recommended (but not enforced) to acquire the mutex before notifying on it. The waiting threads will be unblocked but not continue until the mutex is released (see the threads man page for detailed explanations).
Returns the ID of the thread that holds the mutex.
Returns a list of IDs of those threads currently acquiring the mutex specified by Name, including the one that holds it.
Returns the ID of the thread which most recently notified on the mutex specified by Name.
Returns 1 if the mutex specified by Name exists, 0 otherwise.
Returns a list with all currently existing mutexes.
Sets a thread variable specified by Name to the value specified by Value. If the variable does not exist, it will be created. Thread variables are common for all threads and provide a way of inter-thread communication (similar to static variables in C/C++).
Returns the value of a thread variable specified by Name. If the variable does not exist, an error is returned.
Unsets, i.e. deletes all thread variables specified by the given Names. If a variable does not exist then an error is returned.
Returns 1 if the thread variable specified by Name exists, 0 otherwise.
Returns a list with all currently existing thread variables.
Serializes a piece of code specified by Script. That is, it guarantees that no more than one thread will evaluate Script at any time. If several Scripts are associated with the same Key (an arbitrary string) then no more than one thread will be in ANY of these Scripts. If no Key is specified then internally an empty string will be used as a key. This command is an alternative to explicitely using mutexes and slightly more efficient.
The mutex command internally combines a Tcl_Mutex and a Tcl_Condition (see the threads man page). Dynamically created Tcl_Conditions, either on the heap like with the mutex command or on the stack, seem to leave some memory behind, once they have been used. This even occurs when they are properly finalized and freed up. One Tcl_Condition is also used for each created thread. It is therefore recommended a) to create a limited amount of mutexes and reuse them, and b) to either create a limited amount of threads and reuse them by sending them scripts, or to create as many threads as desired but not to wait for them (i.e. don't use their Tcl_Condition).
When this package is unloaded, e.g. if it has been required in an additional interpreter and then the interpreter is deleted, it is strongly recommended not to have any more threads running.
In Wish for Windows, commands that use the standard channels STDIN, STDOUT and STDERR (such as puts, gets and read) don't work properly within threads. Populating the three standard channels to every new thread (using Tcl_GetStdChannel and Tcl_RegisterChannel) doesn't help but merely creates another memory leak. This leak is also created whenever a thread does e.g. a "puts", because it will automatically create the three standard channels (which did not exist before). This goes for tclsh as well as for wish.
One must be very careful when using the "thread _register_" and "thread _unregister_" commands to share or transfer a channel to another thread, since Tcl's internal linked lists do not really allow to simply call Tcl_RegisterChannel in order to populate a channel to another thread. Depending on the platform different effects might occur, and crashes are likely. Therefore this feature is an experimental one. Some tests under Windows NT and Linux 2.2 showed that
(Yes, it is indeed possible to write a TCP server that distributes incoming connects to separate threads, which later terminate correctly, don't create a memory leak, and leave an intact channel list behind...)
Try this in the Tcl shell tclsh:
% package require mkThreads 1.2 % thread create { after 1000; puts "Thread [thread id] terminates" } 440 % Thread 440 terminates % thread create 250 % thread eval -wait 250 { return RESULT } RESULT % proc p { args } { puts "Args: $args" } % thread eval 250 { after 2000; set AnError } p; after 4000 { set x 0 }; vwait x Args: 1 {can't read "AnError": no such variable} NONE {can't read "AnError": no such variable while executing "set AnError "} % proc e { tid } { puts "Thread $tid exited" } % thread onexit e % thread eval 250 { after 2000; thread exit }; after 4000 { set x 0 }; vwait x Thread 250 exited %
mkThreads is written in C and comes with a DLL for Windows. On Unix, the package needs to be compiled into a shared library first (see below). mkThreads works with Tcl/Tk version 8.1 and higher. It has been stubs-enabled with version 8.2.
Beginning with Tcl 8.3.1, mkThreads 1.2 incorporates the revised thread API and is therefore stubs-enabled. You still should be able to compile and run it for all Tcl versions beginning with 8.1 (where thread support was initially introduced).
Important: Your Tcl/Tk version installed on your computer must have been compiled with thread support. To find out, check for the existence of the global variable "tcl_platform(threaded)". If it does not exist then your Tcl/Tk has not been compiled with thread support. See the Scriptics web site on how to compile it with thread support.
To install, place the directory "mkThreads1.2" in one of the directories contained in the global Tcl variable "auto_path". For a standard Tcl/Tk installation, this is commonly "c:/program files/tcl/lib" (Windows) and "/usr/local/lib" (Unix).
To compile the package, just provide the correct path to "tcl.h" and link against "tcl8x.lib" (Windows) or "libtcl8x.so" (Unix) respectively. If you use stubs, define USE_TCL_STUBS and link against "tclstub8x.lib" (Windows) or "libtclstub8x.a" (Unix) instead.
For Visual C++, the following command should work:
cl /I c:/progra~1/tcl/include /D USE_TCL_STUBS /c mkThreads12.c link c:/progra~1/tcl/lib/tclstub83.lib /dll mkThreads12.obj
On Linux 2.2, this here works fine:
gcc -shared -DUSE_TCL_STUBS -ltclstub8.3 -o mkThreads12.so mkThreads12.c
Test the installation by opening a tclsh or wish and entering "package require mkThreads". The string "1.2" should appear. If it fails, "cd" into the directory "mkThreads1.2" and load it directly with "load ./mkThreads12.dll" (Windows) or "load ./mkThreads12.so" (Unix). If no error occured, it succeeded and something must be wrong with the location of "mkThreads1.2".
Changes from 1.0 to 1.2
Michael Kraus
mailto:michael@kraus5.de
http://mkextensions.sourceforge.net