HOWTO: Make a Tcl Extension Thread-Safe

The basics of making an extension thread-safe are very simple, if a bit crude. You can simply declare a mutex and use that mutex liberally. Each entry point to your code (most likely the entry point of commands you define) can "lock" the code, and then you "unlock" at the exit points. While restrictive, it is effective.

The code for this involves declaring your mutex at the top of the C file like so:

TCL_DECLARE_MUTEX(myMutex)

And then making use of Tcl_MutexLock and Tcl_MutexUnlock in each command, something like:

int Tcl_MyObjCmd(clientData, interp, objc, objv)
    ClientData clientData;	/* As set with Tcl_CreateObjCommand */
    Tcl_Interp *interp;		/* Current interpreter. */
    int objc;			/* Number of arguments. */
    Tcl_Obj *CONST objv[];	/* Argument objects. */
{
    ... [ declaration of variables ] ...

    Tcl_MutexLock(&myMutex);
    if (objc < 2) {
	Tcl_WrongNumArgs(interp, 1, objv, "method ?options?");

	/*
	 * Don't forget to unlock before EVERY exit point
	 */

	Tcl_MutexUnlock(&myMutex);
	return TCL_ERROR;
    }

    ... [ rest of code ] ...

    Tcl_MutexUnlock(&myMutex);
    return TCL_OK;
}

Depending on your code, you could use separate locks for each command, or share the one lock. This of course will add lock overhead throughout your code. BTW, it is perfectly safe to add this to code that is compiled or run against a non-threaded Tcl build, but it does require Tcl 8.1+ header files to build.

The next step to improving use of locks is to fine-tune where the locking is placed. For example, if you only have one or two critical sections that manipulate data that must be serialized, then only lock those areas. In the example above, it is completely unnecessary to do the Tcl_MutexLock before the first argument count check, so that could be moved after down and the first Tcl_MutexUnlock removed. Locks should be placed around any modification to global data, and certain sections of code or external system calls that may not be thread-safe.

After you get your locking fine-tuned, you will notice that in many cases it might be just to protect global data that could otherwise be made per-thread data. You also might need this in the general case for your code. Tcl has APIs for handling this as well. The following is an example instantion of such data:

typedef struct ThreadSpecificData { 
    int initialized;		/* initialization flag */
    Tcl_HashTable *thrHashTbl;	/* per thread hash table. */
} ThreadSpecificData;
static Tcl_ThreadDataKey dataKey;


int Tcl_MyObjCmd(clientData, interp, objc, objv)
    ClientData clientData;	/* As set with Tcl_CreateObjCommand */
    Tcl_Interp *interp;		/* Current interpreter. */
    int objc;			/* Number of arguments. */
    Tcl_Obj *CONST objv[];	/* Argument objects. */
{
    ThreadSpecificData *tsdPtr = (ThreadSpecificData *) 
            Tcl_GetThreadData(&dataKey, sizeof(ThreadSpecificData));
    ... [ declaration of variables ] ...

    if (tsdPtr->initialized == 0) {
	tsdPtr->initialized = 1;
	tsdPtr->thrHashTbl = (Tcl_HashTable *) ckalloc(sizeof(Tcl_HashTable));
	Tcl_InitHashTable(tsdPtr->thrHashTbl, TCL_STRING_KEYS);
    }
    ... [ rest of code ] ...

The Tcl_GetThreadData handles the auto-initialization of all data in the ThreadSpecificData to NULL. Where and how you use this is really specific to your extension.

When compiling your code, you need to make sure TCL_THREADS is defined. If you have a TEA-compliant system, then adding --enable-threads while configuring does this for you, otherwise you can always just add it in by hand.

Joe English adds that another way (which may take a bit longer than 10 minutes depending on how the extension was written) is to avoid using global variables altogether. Instead, wrap all the global data inside a structure, allocate and initialize it in the extension's Xxx_Init() routine, and pass it to command procedures with Tcl_[SG]etAssocData() and/or the 'clientData' parameter.

This way the extension is "thread-oblivious"; Tcl's one-thread-per-interp policy ensures thread safety. As a bonus, the extension is also "multi-interp-safe", i.e., it can be loaded into multiple Tcl_Interps in the same process.

Of course if the extension calls any third-party routines which are not thread-safe then you'll still need to use mutexes. But if not, the thread-oblivious approach is a good alternative.