52.3 Context-Specific Data

Most applications need global variables that have the following characteristics:

With this type of global variable, each context that is reading from or writing to such a variable can read or write its copy of global data without affecting the copy maintained by another context. This concept is similar to thread-specific data (TSD) that is available in many thread libraries.

Associating data with a specific thread can be useful to an application. For example, if the thread is performing some service on behalf of a registered client, the thread needs to save such things as a connection, a directory path, and a credential obtained on behalf of that client so that these can be reused every time the application performs an operation. For LibC, an application needs to maintain errno and other global variables (such as the string currently being processed by strtok or the time string returned by asctime) for each thread.

LibC associates data with contexts instead of threads. This data is called context-specific data (CSD) and has been implemented as key-value pairs. This section describes the following aspects of key-value pairs:

52.3.1 Using Key-Value Pairs

Key-value pairs can be thought of as data arrays that are maintained in each context, and a key is an index into this data array. The act of allocating a key reserves the same slot number for all contexts in the current NLM application and returns a slot number into the data array that is maintained by the specific context. The following figure illustrates this process for a key that has been created for slot 0 in the array.

Figure 52-1 Data Arrays of Keys

When multiple contexts use the same key, each context gets or sets a unique value because it is manipulating the slot that corresponds to its data array key. One context cannot modify the array of another context. In this way, you can create a key and associate it with a deep error value that your code sets inside various functions.

Once a key is allocated, any context (including any contexts that were allocated after the key was allocated) can then associate a value or read the value that it previously associated with the key (see NXKeyCreate).

LibC does not interpret the value associated by a context with a key. To LibC, the array is an array of pointers to void. Therefore, your context can associate any value it chooses with an allocated key in its array.

Because an NLM can be thought of as the environment in which one and only one LibC application runs, migrating threads outside it (a common NetWare programming practice) renders the keys useless. You must make sure that you do not attribute meaning to the keys of a context that a helper NLM might use.

NOTE:Although libraries might make use of keys on behalf of applications, they must be careful if more than one application is calling. In principal, a library should be more interested in the application that a calling thread belongs to than any thread-specific data.

Although the keys and their associated data are conceptually thread specific, they are in fact bound to the context and follow it rather than the thread.

Points to Remember about Keys

  • Keys are part of an NLM, and each NLM manages its keys independently of other NLM applications. Keys cannot be shared with another NLM.

  • Once a key is allocated by any context, it is available for all contexts in the NLM, and it is accessible from any function.

  • A context is able to manipulate its array only; it cannot legally modify the array of another context.

52.3.2 Setting Key Values

Setting and retrieving the values associated with keys are simple operations. Consider the key as an index or subscript into an array. For example:

  • Key 1 is for the path to the client's configuration file.

  • Key 2 is for a pointer to a connection structure with the remote server name.

Setting an integer, enumeration (enum), or other scalar value for a key involves casting the value to void*. Call NXKeySetValue on each context to establish a value that accurately represents the context.

Points to Remember about Key Values

  • A key has to be allocated before any context can associate a value with it.

  • NULL is the default initial value associated with each context for an allocated key.

  • A context can associate a value for an allocated key and can subsequently read the value it associated earlier with the key.

52.3.3 Specifying a Destructor Function

LibC allows you to specify a destructor function when you allocate a key. This destructor function is the same across all contexts and thus cannot be subsequently modified. You are responsible for defining and managing the destructor. Because all contexts might not use a key and set its value, the destructor must be able to handle a key with a zero value.

The destructor must match the data type of the key. For example, if the data associated with the key has been allocated by calling NXMemAlloc, the passed destructor might be NXMemFree because the NXMemFree accepts void* as its only argument.

Points to Remember about the Destructor Function

  • A destructor function can be specified only at key allocation time and cannot be modified subsequently.

  • The destructor function must match the data type of the key.

  • The destructor function must be able to handle a key with a zero value.

  • A destructor function applies across all contexts so that each context cannot set a different destructor for the same key.

  • A destructor function is called when a context is destroyed, reinitialized, or unbound from a thread. It is not called when a thread swaps out one context for another context.

52.3.4 Deleting and Reinitializing Keys

When a context is destroyed, unbound from a thread, or reinitialized, LibC walks through the array for the context. For each allocated key, it calls the destructor with the value associated by the context with the key passed as a parameter. LibC then sets the value associated with the key to NULL for the context so that all values associated with keys by that context are lost.

The lifetime of an allocated key is not dependent on the context that created the key. It remains allocated even if the context that originally created the key is destroyed. For example, your application might create a login key for the login structure that contains the user's name, assigned privileges, and home directory. If this key is created when the first user logs in, you would not want that key destroyed when the first user logs out. The key is still needed by other users who are still logged in and by new users who will log in.

Usually keys should not be destroyed; they should remain allocated as long as the application lives. However, the NKS interface does include an NXKeyDelete function that deletes a key. This function does not call the destructor function for every context because the data might be in use by other threads. Before deleting a key, you are responsible for ensuring the following:

  • No context is currently using the key.

  • All contexts have set the key value to NULL.

  • No context will subsequently try to access the destroyed key and use its data or set its value. Because the key is an index into an array, a newly created key with a different data type could be assigned the same index.

If possible, avoid calling NXKeyDelete.

Points to Remember

  • A key is designed to live for the lifetime of the application, and its lifetime is not tied to the context that created it.

  • A key's value is reinitialized to NULL when the associated context is destroyed, the associated thread is unbound from the context, or a specific request is made to reinitialize the context.

  • Destructors are not called when a key is deleted.

52.3.5 Using Thread-Specific or Instance Data

The strtok function (as well as functions such as errno, unitok, asctime, and localeconv) has per-thread or per-VM static data. It’s called instance data.

You can create the same type of instance data using NXKeyCreate. The following code snippets illustrate this process. They assume that you want to “color” each thread according to the rainbow for whatever utility this might be.

  1. To create the gRainbowColorKey:

      #include <nks/thread.h>
      
      int gRainbowColorKey;
      
      int main( int argc, char **argv[] )
      {
         int err;
         ...   err = NXKeyCreate(free, NULL, &gRainbowColorKey);
      }
      
      
  2. To create the specific thread's data:

      int foo( void )
      {
         int  err; 
         char *color; 
      
         color = malloc(MAX_COLOR_LEN);
      
         if (!color)
            return -1;
      
         strcpy(color, "indigo");
      
         err = NXKeySetValue(gRainbowColorKey, color);
         ...
      }
      
      
  3. To see what the calling thread has for its "Rainbow color" value:

      int oof( void )
      {
         int  err;
         char *color;
      
         ...
      
         err = NXKeyGetValue(gRainbowColorKey, &color);   if (strcmp(color, "red") == 0)      ...;   else if (strcmp(color, "indigo") == 0)      ...;
         ...
      }
      
      

This is a primitive example. When you create the key, you can register a destructor that will accurately dispose of the key data when the thread dies. In this case, the value is merely memory allocated on the heap, but an arbitrarily more complex example could have rainbow color data being a complicated data structure such as a binary tree with a destructor function traversing it in post-order, freeing the individual nodes, etc. Or, it can merely be an integer value that needs no destruction in which case the first argument to NXKeyCreate should be passed as NULL.

This is exactly what keys exist to accomplish: formalized per-thread specific data.