2.9.3 Thread-Local Variables

DTrace provides the ability to declare variable storage that is local to each operating system thread, as opposed to the global variables demonstrated earlier in this chapter. Thread-local variables are useful in situations where you want to enable a probe and mark every thread that fires the probe with some tag or other data. Creating a program to solve this problem is easy in D because thread-local variables share a common name in your D code, but refer to separate data storage that is associated with each thread.

Thread-local variables are referenced by applying the -> operator to the special identifier self, for example:

syscall::read:entry
{
  self->read = 1;
}

This D fragment example enables the probe on the read() system call and associates a thread-local variable named read with each thread that fires the probe. Similar to global variables, thread-local variables are created automatically on their first assignment and assume the type that is used on the right-hand side of the first assignment statement, which is int in this example.

Each time the self->read variable is referenced in your D program, the data object that is referenced is the one associated with the operating system thread that was executing when the corresponding DTrace probe fired. You can think of a thread-local variable as an associative array that is implicitly indexed by a tuple that describes the thread's identity in the system. A thread's identity is unique over the lifetime of the system: if the thread exits and the same operating system data structure is used to create a new thread, this thread does not reuse the same DTrace thread-local storage identity.

When you have defined a thread-local variable, you can reference it for any thread in the system, even if the variable in question has not been previously assigned for that particular thread. If a thread's copy of the thread-local variable has not yet been assigned, the data storage for the copy is defined to be filled with zeroes. As with associative array elements, underlying storage is not allocated for a thread-local variable until a non-zero value is assigned to it. Also, as with associative array elements, assigning zero to a thread-local variable causes DTrace to deallocate the underlying storage. Always assign zero to thread-local variables that are no longer in use. For other techniques to fine-tune the dynamic variable space from which thread-local variables are allocated, see Chapter 10, Options and Tunables.

Thread-local variables of any type can be defined in your D program, including associative arrays. The following are some example thread-local variable definitions:

self->x = 123; /* integer value */

self->s = "hello"; /* string value */

self->a[123, 'a'] = 456; /* associative array */

Like any D variable, you do not need to explicitly declare thread-local variables prior to using them. If you want to create a declaration anyway, you can place one outside of your program clauses by pre-pending the keyword self, for example:

self int x; /* declare int x as a thread-local variable */ 
syscall::read:entry
{
  self->x = 123;
}

Thread-local variables are kept in a separate namespace from global variables so that you can reuse names. Remember that x and self->x are not the same variable if you overload names in your program.

The following example shows how to use thread-local variables. In an editor, type the following program and save it in a file named rtime.d:

syscall::read:entry
{
  self->t = timestamp;
}

syscall::read:return
/self->t != 0/
{
  printf("%d/%d spent %d nsecs in read()\n", pid, tid, timestamp - self->t);
  /* 
   * We are done with this thread-local variable; assign zero to it
   * to allow the DTrace runtime to reclaim the underlying storage.
   */ 
  self->t = 0;
}

Next, in your shell, start the program running. Wait a few seconds and you should begin to see some output. If no output appears, try running a few commands:

# dtrace -q -s rtime.d
3987/3987 spent 12786263 nsecs in read()
2183/2183 spent 13410 nsecs in read()
2183/2183 spent 12850 nsecs in read()
2183/2183 spent 10057 nsecs in read()
3583/3583 spent 14527 nsecs in read()
3583/3583 spent 12571 nsecs in read()
3583/3583 spent 9778 nsecs in read()
3583/3583 spent 9498 nsecs in read()
3583/3583 spent 9778 nsecs in read()
2183/2183 spent 13968 nsecs in read()
2183/2183 spent 72076 nsecs in read()
...
^C
#

The rtime.d program uses a thread-local variable that is named to capture a timestamp on entry to read() by any thread. Then, in the return clause, the program prints the amount of time spent in read() by subtracting self->t from the current timestamp. The built-in D variables pid and tid report the process ID and thread ID of the thread that is performing the read(). Because self->t is no longer needed after this information is reported, it is then assigned 0 to enable DTrace to reuse the underlying storage that is associated with t for the current thread.

Typically, you see many lines of output without doing anything because server processes and daemons are executing read() all the time behind the scenes. Try changing the second clause of rtime.d to use the execname variable to print out the name of the process performing a read(), for example:

printf("%s/%d spent %d nsecs in read()\n", execname, tid, timestamp - self->t);

If you find a process that is of particular interest, add a predicate to learn more about its read() behavior, as shown in the following example:

syscall::read:entry
/execname == "Xorg"/
{
  self->t = timestamp;
}