TIP 218: Tcl Channel Driver Thread State Actions

Login
Author:         Andreas Kupries <[email protected]>
Author:         Andreas Kupries <[email protected]>
Author:         Larry W. Virden <[email protected]>
Author:         David Gravereaux <[email protected]>
State:          Final
Type:           Project
Vote:           Done
Created:        09-Sep-2004
Post-History:   
Tcl-Version:    8.5
Tcl-Ticket:     875701

Abstract

This document specifies a modification of the public definition of channel drivers for the Tcl I/O system to fix a bug in the transfer of channels between threads affecting all drivers requiring thread-specific initialization. The targets for the change are Tcl 8.4.x (note that this is an API change to a release) and Tcl 8.5.

Background and Motivation

The purpose of this TIP is to address and solve a general problem exposed through the actual usage of the ability to transfer any channel between threads. Note that this ability was introduced into the Tcl core before TIPs were used to manage major changes, therefore it is not possible for us here to refer to a TIP. The relevant entry in the Tcl core ChangeLog is of 02-May-2000.

The specific instance of the general problem is logged as SourceForge tracker item 952332http://sf.net/support/tracker.php?aid=952332 (a Feature Request in the IOCPSock project). The Socket Channel Driver in the core maintains state not only per channel, but on Windows also per thread. This thread specific state is initialized during the first usage of the socket command at the script-level, i.e. Tcl_SocketObjCmd() at the C-level. The relevant internal function for this is TclpHasSockets().

This causes problems when transferring a socket channel into a thread which has not performed any socket operation before, as the per-thread state of the driver is not initialized at all in that case, causing a crash when trying to use the transfered channel.

This is a general problem all channel drivers maintaining per-thread state will run into. And on Windows most channel drivers will have to maintain per-thread state. This is imposed by the design of the Tcl Notifier. It manages Event Sources on a per-thread basis, and this implies that the Event Source for channel drivers has to maintain, per-thread, a list of the channels belonging to a thread to make setup and checking for events efficient. A process global list would be possible, but would also not be very fast, as it would require locking for each search through it, and such searches happens per iteration per active eventloop per Tcl thread.

What is needed is some way to initialize the thread-specific state of a channel driver for a specific thread, whenever a channel is created within that thread, independent of the method of creation. This can be solved without API changes, if we would restrict ourselves to the channel drivers in the core. This however would make non-core channel drivers second class. To avoid this an official public API is required.

Note that the currently used fix in both 8.4 and 8.5 is an ad-hoc solution without API changes, making external drivers second class. In 8.4 the ad-hoc solution is even incomplete, not covering the sockets, only files.

This TIP has been written to remedy this deficiency for both 8.4 and 8.5.

Specification

It is possible to solve the specific problem described in the last chapter, by calling the relevant internal function, either in each low-level socket operation, or during the transfer itself. The first will slow down all socket operations (one additional check for per-thread initialization per operation (read, write, seek, ...)), the second introduces code specific to channel types into the transfer code. Neither solution is considered acceptable; they are mere band-aids.

Instead of using one of the band-aids described above, this TIP proposes to solve the general problem of per-thread channel driver initialization by extending the public structure of channel drivers with one new function pointer.

The purpose of this function is not directly the initialization of per-thread information, but to provide the driver with notifications when a channel of its type is removed from or added to a thread, by whatever means (creation, deletion, transfer, ...). This can be used not only to initialize the per-thread data of the channel driver, but for other thread-specific actions of the driver as well.

As we are modifying a public structure, we have to distinguish between older drivers not supporting the new function and new drivers which do. The 'Tcl_ChannelType' structure contains a version field just for this purpose. Currently known versions of drivers are version 0 to 3, where version 0 is for drivers whose structure does not contain a version field. The modification proposed in this document now introduces channel driver version 4.

Details

The definition of the new version tag is

 #define TCL_CHANNEL_VERSION_4 ((Tcl_ChannelTypeVersion) 0x4)

The signature of the new function is

 void Tcl_DriverThreadActionProc (ClientData instanceData, int action);

 /* Codes for 'action' */

 #define TCL_CHANNEL_THREAD_INSERT (0)
 #define TCL_CHANNEL_THREAD_REMOVE (1)

The Tcl_DriverThreadActionProc defined for a channel type FOO will be called by the core with thread BAR as current thread whenever:

  1. a channel of type FOO is created in thread BAR.

  2. a channel of type FOO is transfered into the thread BAR.

  3. a channel of type FOO is closed in thread BAR.

  4. a channel of type FOO is transfered out of the thread BAR.

For the first two calls we can assert action == TCL_CHAN_THREAD_INSERT, and the last two calls can assert action == TCL_CHAN_THREAD_REMOVE.

Note that multiple calls of Tcl_DriverThreadActionProc per thread are possible and have to be dealt with correctly.

The new definition of structure Tcl_ChannelType is

 typedef struct Tcl_ChannelType {
 [... Existing Definition ...]
     /*
      * Only valid in TCL_CHANNEL_VERSION_4 channels or later
      */
     Tcl_DriverThreadActionProc *threadActionProc;
                                       /* Procedure to call to notify
                                        * the driver of thread specific
                                        * activity for a channel.
                                        * May be NULL. */
 } Tcl_ChannelType;

A new public accessor API is provided as well, returning a pointer to the new function (or NULL if it does not exist).

 Tcl_DriverThreadActionProc *
 Tcl_ChannelThreadActionProc(Tcl_ChannelType* typePtr);

Discarded Solutions

It was initially proposed to extend the driver structure with a function having only a very narrow purpose- the initialization of the thread specific data of the driver.

This was dropped because the solution actually proposed is more general and thus should be more stable in the long term. In other words, we believe that the adoption of the general solution reduces the risk of having to extend the Channel driver API again in the future, compared to the risk of this should we adopt the narrow solution.

Another approach uses the same model as specified here, i.e. general cut and splice functionality, but uses two driver functions to implement this. The problem with this approach is that an implementor of a driver might get confused and realize only one of the two functions, i.e. only part of the required functionality.

Reference Implementation

A reference implementation is provided at SourceForge http://sourceforge.net/support/tracker.php?aid=875701 .

Comments

David Gravereaux has recently tested this with his IOCPSOCK extension (11/26) and found no problems. If it hastens the acceptance of the TIP, David does not need the one export in the patch (Tcl_DriverThreadActionProc) to be exported for his extension to work. Others that do channel chaining (windows expect, for example), will most likely need it.

Copyright

This document has been placed in the public domain.