Thread- Local Storage of Data in .NET

Suppose you're writing a multi-threaded application and you want each thread to have its own copy of some data. You also want this data to persist throughout the lifetime of the thread.

Introduction

Suppose you're writing a multi-threaded application and you want each thread to have its own copy of some data. You also want this data to persist throughout the lifetime of the thread.

As each thread has its own stack, it also has its own copy of any local variables. However, the trouble with these is that they are not persistent - once the current method has finished executing they're destroyed.

Also local variables are scoped to the current method and so, if you want them to be available to other methods which the current method calls, then you need to pass them as parameters. This can get rather messy if there are several such variables and multiple method calls.

You could use a static field to store persistent data but then there's only one copy of the data which all threads share.

So is there anything you can use so that each thread has its own copy of persistent data?

The answer is Thread-Local Storage or TLS for short.

There are three ways to create TLS using a .NET application each of which is discussed in turn below.

The ThreadStatic attribute

If you decorate a static field with the ThreadStatic attribute, then each thread will have its own copy of that field. Here's an example:

using System;
using System.Threading;

class ThreadStaticTest
{
    [ThreadStatic]
    static string greeting;

    static void Main()
    {
        greeting = "Goodbye from the main thread";
        Thread t = new Thread(ThreadMethod);
        t.Start();
        t.Join();
        Console.WriteLine(greeting); // prints the main thread's copy
        Console.ReadKey();
    }

    static void ThreadMethod()
    {
        greeting = "Hello from the second thread"; // only affects the second thread's copy
        Console.WriteLine(greeting);
    }
}

The output is:

Thread1.gif

However, the ThreadStatic attribute has a couple of shortcomings:

  • It doesn't work with instance fields.

  • If you initialize the static field to some non-default value (either where it's defined or using a static constructor), then it gets initialized only once for the thread which is running at the time. Other threads only get its default value.

The effect of the second problem can be seen by running this modified version of the above program:

class ThreadStaticTest2
{
    [ThreadStatic]
    static string greeting = "Greetings from the current thread";

    static void Main()
    {
        Console.WriteLine(greeting); // prints initial value
        greeting = "Goodbye from the main thread";
        Thread t = new Thread(ThreadMethod);
        t.Start();
        t.Join();
        Console.WriteLine(greeting); // prints the main thread's copy
        Console.ReadKey();
    }

    static void ThreadMethod()
    {
        Console.WriteLine(greeting); // prints nothing as greeting initialized on main thread
        greeting = "Hello from the second thread"; // only affects the second thread's copy
        Console.WriteLine(greeting);
    }
}      


The output is now:

Thread2.gif

Notice that the second line is blank because the second thread's copy of 'greeting' is null, initially.

The ThreadLocal<T> class

This is a class which was introduced in .NET 4.0 and which solves the problems with the ThreadStatic attribute.

Here's a similar program but this time using ThreadLocal<T> for both an instance and a static field and providing them with initial non-default values:

using System;
using System.Threading;

class ThreadLocalTest
{
    static ThreadLocal<string> greeting;
    ThreadLocal<int> numThreads = new ThreadLocal<int>(() => 1);
// instance field

    static ThreadLocalTest()
    {
        greeting = new ThreadLocal<string>(() => "Greetings from the current thread");
    }

    static void Main()
    {
        Console.WriteLine(greeting.Value.Replace("current", "main"));
        greeting.Value = "Goodbye from the main thread";
        Thread t = new Thread(ThreadMethod);
        t.Start();
        t.Join();
        ThreadLocalTest tl = new ThreadLocalTest();
        Thread t2 = new Thread(tl.InstanceThreadMethod);
        t2.Start();
        t2.Join();
        Console.WriteLine("The number of current threads is now " + tl.numThreads);
        Console.WriteLine(greeting.Value); // prints the main thread's copy which is still 1
        Console.ReadKey();
    }

    static void ThreadMethod()
    {
        Console.WriteLine(greeting.Value.Replace("current", "second"));
        greeting.Value = "Hello from the second thread"; // only affects the second thread's copy
        Console.WriteLine(greeting.Value);
    }

    void InstanceThreadMethod()
    {
        numThreads.Value++; // increment this thread's copy to 2
        Console.WriteLine("The number of current threads is " + this.numThreads);
    }
}

The output is:

Thread3.gif

Notice that the ThreadLocal<T> constructor takes a Func<T> argument i.e. a delegate for a method with no parameters which returns a value of type T. Here we're passing it a compatible lambda expression.

Also the thread local variable is initialized 'lazily' i.e. it's not initialized until a thread first attempts to retrieve its Value property. You can check whether it's been initialized yet for the current thread with the IsValueCreated property.

Thread.SetData and Thread.GetData

These two static methods store and retrieve data for each thread in a local data store 'slot'. If you give this slot a name then it can be accessed throughout the application using that name, no matter where it was originally allocated.

This doesn't apply to unnamed slots which are only accessible if allocated within the current scope or exposed via a globally accessible variable.

Each thread in the application can store its own copy of the data within a single named or unnamed slot.

Here's a version of the earlier programs which uses a named slot:

using System;
using System.Threading;

class ThreadNamedSlotTest
{
    static LocalDataStoreSlot greeting = Thread.AllocateNamedDataSlot("greeting");

    static void Main()
    {
        Thread.SetData(greeting, "Goodbye from the main thread");
        Thread t = new Thread(ThreadMethod);
        t.Start();
        t.Join();
        Console.WriteLine(Thread.GetData(greeting)); // prints the main thread's copy
        Thread.FreeNamedDataSlot("greeting"); // releases across all threads
        Console.ReadKey();
    }

    static void ThreadMethod()
    {
        Thread.SetData(greeting, "Hello from the second thread"); // only affects the second thread's copy
        Console.WriteLine(Thread.GetData(greeting));
        OtherClass oc = new OtherClass();
        oc.Print();
    }
}

class OtherClass
{
    public void Print()
    {
        Console.WriteLine("The next line is printed from other class ...");
        LocalDataStoreSlot greeting = Thread.GetNamedDataSlot("greeting");
        Console.WriteLine(Thread.GetData(greeting));
    }
}

The output is:

Thread4.gif

To use an unnamed slot, the declaration of the 'greeting' field would need to be changed to:

static LocalDataStoreSlot greeting = Thread.AllocateDataSlot();

However, unless you made this field public, it would not then be accessible from OtherClass.

Using data slots has a couple of drawbacks:

  • They're slower than the other two TLS approaches.

  • The data is not strongly typed. Both the SetData and GetData methods take parameters of type Object.

Conclusion

Prior to the advent of .NET 4.0, thread-local storage was a little known (and understood) aspect of .NET's threading support even though the underlying mechanism is provided by the operating system itself.

However, it has assumed a greater importance with the introduction of parallel programming support and the ThreadLocal<T> class in .NET 4.0. This is because it enables each thread to access its own copy of an object rather than a single shared object and thereby achieve thread-safety without the need for locks.

The examples used in this article are also available in the accompanying download for anyone who would like to play around with the code.