Tcl HomeTcl Home Hosted by
ActiveState

Google SiteSearch

This page describes the model Tcl uses for multi-threading, and it gives an overview of the APIs available at the Tcl and C level. The Thread Extension exposes the threading facilities described here to the Tcl script level.

One Thread Per Interpreter

Tcl lets you have one or more Tcl interpreters (e.g., created with Tcl_CreateInterp()) in each operating system thread. However, each interpreter is tightly bound to its OS thread and errors will occur if you let more than one thread call into the same interpreter (e.g., with Tcl_Eval).

Communication Among Threads

Tcl scripts in different threads can communicate by posting scripts onto the event queue of an interpreter in a different thread. This can be synchronous, where you wait for the result, or asynchronous, where your thread does not wait for the other thread to evaluate its script.

The 2.x version of the Thread Extension provides shared variables, mutexes, and condition variables so you can enjoy all the benefits, perils, and pitfalls of threaded programming. Also, if you use this extension with the new 8.4 releases you can transfer I/O channels between threads.

The Thread Package

By default, Tcl 8.4 is compiled without thread support and without script-level access to threads. To use threads you can use the testthread command that was added to tcltest for the Tcl test suite. This was turned into its own extension, Thread extension noted above, in conjunction with the Tcl 8.3.1 release. There is also a mkThread extension created by Michael Kraus. So, now you can just build Tcl 8.3 (or 8.4) with threads enabled and load the thread extension.

The C API

If you maintain an extension, you'll need to use the C APIs to make your extension thread safe. Tcl provides mutex locks, condition variables for synchronization, and thread-local storage to help manage data structures. In addition to this documentation, see the Thread-Safing Extensions page by Jeff Hobbs.

If you are making old code thread safe, then you can focus your attention on the global data structures. You'll either need to serialize all thread access by putting a Tcl_MutexLock and Tcl_MutexUnlock call around all accesses to the variable, or you may be able to move the data structure into "thread local storage".

A great source of examples is the Tcl and Tk source code itself. The following are examples taken from the sources.

Mutex Variable Example

The tclEvent.c file maintains a global list of exit handlers. Access to this list is serialized with a mutex lock. The TCL_DECLARE_MUTEX macro declares a mutex variable if threading is enabled, otherwise it does nothing.

static ExitHandler *firstExitPtr = NULL;
TCL_DECLARE_MUTEX(exitMutex)

Later, in the code that references firstExitPtr:


Tcl_MutexLock(&exitMutex);
exitPtr->nextPtr = firstExitPtr;
firstExitPtr = exitPtr;
Tcl_MutexUnlock(&exitMutex);

The Tcl_MutexLock and Tcl_MutexUnlock calls are also macros that expand into nothing unless you configure with --enable-threads.

Thread Local Storage / ThreadSpecificData

In many cases it is possible to have storage that is private to a thread instead of shared among threads. Access to this is cheaper because you do not need to synchronize. For example, Tcl keeps a list of I/O channels that are opened by a particular thread. As there is no sharing among interpreters in different threads, this information can be managed by each thread independently.

If you look in the Tcl sources for


typedef struct ThreadSpecificData
you will find several examples. These are per-file declarations of the thread-local variables used in that file. For example, in tclIO.c,

typedef struct ThreadSpecificData {
 
    /*
     * This variable holds the list of nested ChannelHandlerEventProc
     * invocations.
     */
    NextChannelHandler *nestedHandlerPtr;
 
    /*
     * List of all channels currently open.
     */
    Channel *firstChanPtr;

    /*
     * Static variables to hold channels for stdin, stdout and stderr.
     */
    Tcl_Channel stdinChannel;
    int stdinInitialized;
    Tcl_Channel stdoutChannel;
    int stdoutInitialized;
    Tcl_Channel stderrChannel;
    int stderrInitialized;
 
} ThreadSpecificData;
 
static Tcl_ThreadDataKey dataKey;
Each block of thread specific data is associated with a thread "data key", which is an identifier for this particular block of thread specific data. Each thread will use the same identifier to get its own private copy of those variables. It works like this (e.g., in TclFinalizeIOSubsystem):

void
TclFinalizeIOSubsystem()
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);
    Channel *chanPtr;                   /* Iterates over open channels. */
    Channel *nextChanPtr;               /* Iterates over open channels. */
 
 
    for (chanPtr = tsdPtr->firstChanPtr; chanPtr != (Channel *) NULL;
             chanPtr = nextChanPtr) {
        nextChanPtr = chanPtr->nextChanPtr;
 
	/* code omitted */
    }
}

Condition Variables

Condition variables are associated with a Mutex lock. The mutex is automatically released when you wait on a condition variable, and the mutex is automatically aquired when you unblock from waiting. The typical patterns of use are:


Tcl_MutexLock(&myMutex);

while (condition_is_false) {
     Tcl_ConditionWait(&myConditionVariable, &myMutex, NULL /* no timeout */);
}

Tcl_MutexUnlock(&myMutex);
and

Tcl_MutexLock(&myMutex);
/* set shared state */
Tcl_ConditionNotify(&myConditionVariable);
Tcl_MutexUnlock(&myMutex);

Thread C API documentation

Allocation and Cleanup Issues

The Mutex, Condition Variable, and Thread Local Storage structures in Tcl are "self-initializing". For example, there is Tcl_MutexLock and Tcl_MutexUnlock, but no Tcl_MutexInit. Similarly, the first time a thread fetches a block of thread-specific data, it is automatically allocated and initialized to all zeros.

These objects are also cleaned up automatically when a thread is terminated. The thread-specific data is cleaned up early, right after the per-thread exit handlers are called. If you need to clean up information associated with thread specific data, use a per-thread exit handler to do it.