altandogan
altandogan

Reputation: 1285

Thread Local Storage working principle

This is an example about Thread Local Storage (TLS) from Apress parallel programming book. I know that if we have 4 cores computer 4 thread can run parallel in same time. In this example we create 10 task and we suppose that have 4 cores computer. Each Thread local storage live in on thread so when start 10 task parallel only 4 thread perform. And We have 4 TLS so 10 task try to change 4 Thread local storage object. i want to ask how Tls prevent data race problem when thread count < Task count ??

using System;
using System.Threading;
using System.Threading.Tasks;
namespace Listing_04
{
    class BankAccount
    {
        public int Balance
        {
            get;
            set;
        }
    }
    class Listing_04
    {
        static void Main(string[] args)
        {
            // create the bank account instance
            BankAccount account = new BankAccount();
            // create an array of tasks
            Task<int>[] tasks = new Task<int>[10];
            // create the thread local storage
            ThreadLocal<int> tls = new ThreadLocal<int>();
            for (int i = 0; i < 10; i++)
            {
                // create a new task
                tasks[i] = new Task<int>((stateObject) =>
                {
                    // get the state object and use it
                    // to set the TLS data
                    tls.Value = (int)stateObject;
                    // enter a loop for 1000 balance updates
                    for (int j = 0; j < 1000; j++)
                    {
                        // update the TLS balance
                        tls.Value++;
                    }
                    // return the updated balance
                    return tls.Value;
                }, account.Balance);
                // start the new task
                tasks[i].Start();
            }
            // get the result from each task and add it to
            // the balance
            for (int i = 0; i < 10; i++)
            {
                account.Balance += tasks[i].Result;
            }
            // write out the counter value
            Console.WriteLine("Expected value {0}, Balance: {1}",
            10000, account.Balance);
            // wait for input before exiting
            Console.WriteLine("Press enter to finish");
            Console.ReadLine();
        }
    }
}

Upvotes: 0

Views: 1251

Answers (1)

Peter Duniho
Peter Duniho

Reputation: 70652

We have 4 TLS so 10 task try to change 4 Thread local storage object

In your example, you could have anywhere between 1 and 10 TLS slots. This is because a) you are not managing your threads explicitly and so the tasks are executed using the thread pool, and b) the thread pool creates and destroys threads over time according to demand.

A loop of only 1000 iterations will completely almost instantaneously. So it's likely all ten of your tasks will get through the thread pool before the thread pool decides a work item has been waiting long enough to justify adding any new threads. But there is no guarantee of this.

Some important parts of the documentation include these statements:

By default, the minimum number of threads is set to the number of processors on a system

and

When demand is low, the actual number of thread pool threads can fall below the minimum values.

In other words, on your four-core system, the default minimum number of threads is four, but the actual number of threads active in the thread pool could in fact be less than that. And if the tasks take long enough to execute, the number of active threads could rise above that.

The biggest thing to keep in mind here is that using TLS in the context of a thread pool is almost certainly the wrong thing to do.

You use TLS when you have control over the threads, and you want a thread to be able to maintain some data private or unique to that thread. That's the opposite of what happens when you are using the thread pool. Even in the simplest case, multiple tasks can use the same thread, and so would wind up sharing TLS. And in more complicated scenarios, such as when using await, a single task could wind up executed in different threads, and so that one task could wind up using different TLS values depending on what thread is assigned to that task at that moment.

how Tls prevent data race problem when thread count < Task count ??

That depends on what "data race problem" you're talking about.

The fact is, the code you posted is filled with problems that are at the very least odd, if not outright wrong. For example, you are passing account.Balance as the initial value for each task. But why? This value is evaluated when you create the task, before it could ever be modified later, so what's the point of passing it?

And if you thought you were passing whatever the current value is when the task starts, that seems like that would be wrong too. Why would it be valid to make the starting value for a given task vary according to how many tasks had already completed and been accounted for in your later loop? (To be clear: that's not what's happening…but even if it were, it'd be a strange thing to do.)

Beyond all that, it's not clear what you thought using TLS here would accomplish anyway. When each task starts, you reinitialize the TLS value to 0 (i.e. the value of account.Balance that you've passed to the Task<int> constructor). So no thread involved ever sees a value other than 0 during the context of executing any given task. A local variable would accomplish exactly the same thing, without the overhead of TLS and without confusing anyone who reads the code and tries to figure out why TLS was used when it adds no value to the code.

So, does TLS solve some sort of "data race problem"? Not in this example, it doesn't appear to. So asking how it does that is impossible to answer. It doesn't do that, so there is no "how".


For what it's worth, I modified your example slightly so that it would report the individual threads that were assigned to the tasks. I found that on my machine, the number of threads used varied between two and eight. This is consistent with my eight-core machine, with the variation due to how much the first thread in the pool can get done before the pool has initialized additional threads and assigned tasks to them. Most commonly, I would see the first thread completing between three and five of the tasks, with the remaining tasks handled by remaining individual threads.

In each case, the thread pool created eight threads as soon as the tasks were started. But most of the time, at least one of those threads wound up unused, because the other threads were able to complete the tasks before the pool was saturated. That is, there is overhead in the thread pool just managing the tasks, and in your example the tasks are so inexpensive that this overhead allows one or more thread pool threads to finish one task before the thread pool needs that thread for another.

I've copied that version below. Note that I also added a delay between trial iterations, to allow the thread pool to terminate the threads it created (on my machine, this took 20 seconds, hence the delay time hard-coded…you can see the threads being terminated in the debugger output).

static void Main(string[] args)
{
    while (_PromptContinue())
    {
        // create the bank account instance
        BankAccount account = new BankAccount();
        // create an array of tasks
        Task<int>[] tasks = new Task<int>[10];
        // create the thread local storage
        ThreadLocal<int> tlsBalance = new ThreadLocal<int>();
        ThreadLocal<(int Id, int Count)> tlsIds = new ThreadLocal<(int, int)>(
            () => (Thread.CurrentThread.ManagedThreadId, 0), true);
        for (int i = 0; i < 10; i++)
        {
            int k = i;
            // create a new task
            tasks[i] = new Task<int>((stateObject) =>
            {
                // get the state object and use it
                // to set the TLS data
                tlsBalance.Value = (int)stateObject;
                (int id, int count) = tlsIds.Value;
                tlsIds.Value = (id, count + 1);
                Console.WriteLine($"task {k}: thread {id}, initial value {tlsBalance.Value}");
                // enter a loop for 1000 balance updates
                for (int j = 0; j < 1000; j++)
                {
                    // update the TLS balance
                    tlsBalance.Value++;
                }
                // return the updated balance
                return tlsBalance.Value;
            }, account.Balance);
            // start the new task
            tasks[i].Start();
        }

        // Make sure this thread isn't busy at all while the thread pool threads are working
        Task.WaitAll(tasks);

        // get the result from each task and add it to
        // the balance
        for (int i = 0; i < 10; i++)
        {
            account.Balance += tasks[i].Result;
        }

        // write out the counter value
        Console.WriteLine("Expected value {0}, Balance: {1}", 10000, account.Balance);
        Console.WriteLine("{0} thread ids used: {1}",
            tlsIds.Values.Count,
            string.Join(", ", tlsIds.Values.Select(t => $"{t.Id} ({t.Count})")));
        System.Diagnostics.Debug.WriteLine("done!");
        _Countdown(TimeSpan.FromSeconds(20));
    }
}

private static void _Countdown(TimeSpan delay)
{
    System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew();

    TimeSpan remaining = delay - sw.Elapsed,
        sleepMax = TimeSpan.FromMilliseconds(250);
    int cchMax = $"{delay.TotalSeconds,2:0}".Length;
    string format = $"\r{{0,{cchMax}:0}}", previousText = null;

    while (remaining > TimeSpan.Zero)
    {
        string nextText = string.Format(format, remaining.TotalSeconds);

        if (previousText != nextText)
        {
            Console.Write(format, remaining.TotalSeconds);
            previousText = nextText;
        }
        Thread.Sleep(remaining > sleepMax ? sleepMax : remaining);
        remaining = delay - sw.Elapsed;
    }

    Console.Write(new string(' ', cchMax));
    Console.Write('\r');
}

private static bool _PromptContinue()
{
    Console.Write("Press Esc to exit, any other key to proceed: ");
    try
    {
        return Console.ReadKey(true).Key != ConsoleKey.Escape;
    }
    finally
    {
        Console.WriteLine();
    }
}

Upvotes: 3

Related Questions