[C# thread] some actual threads local storage area (TLS) ThreadStatic|LocalDataStoreSlot|ThreadLocal<T >

Posted by genius on Mon, 27 Dec 2021 23:33:19 +0100

Example of putting apples in a bag

Most articles on C# multithreading discuss the start and stop of threads or multithread synchronization. Multithread synchronization is to access the same variable (usually the variable outside the thread working function) in different threads. It is well known that without the thread synchronization mechanism, some threads will produce dirty reads or overwrite the written values of other threads due to the existence of concurrency Another situation is that we want the variables accessed by the thread to belong to the thread itself, which is the so-called thread local variables.
Below, we will gradually expand the simplest example code to show the differences and respective solutions of variable concurrent access and thread local variables mentioned above.

The example shown here is simple. The accessed variable is "the number of apples in the bag", and the working function is "put apples in the bag".

public class Bag
{
    public int AppleNum { get; set; }
}

public class Test
{
    public void TryTwoThread()
    {
        var b = new Bag();
        Action localAct = () =>
        {
            for (int i = 0; i < 10; i++)
            {    
                ++b.AppleNum;
                Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} - {b.AppleNum}");
                Thread.Sleep(100);
            }
        };
        Parallel.Invoke(localAct, localAct);
    }
}

// Program.cs
var tester = new Test();
tester.TryTwoThread();

As shown in the code, this is a classic multi-threaded variable concurrent access error code. Since there is no code for concurrency access control, the execution result is uncertain. The expected result is that there are 20 apples planted in bags, which is difficult to achieve in practice.

 

 

Demonstration of the correct case of putting apples in the bag

Because the execution result is uncertain, the above shows only one random case.

The way to solve this problem is to use concurrency control. The easiest way is to lock the access to shared variables.

public class Test
{
    private object _locker = new object();

    public void TryTwoThread()
    {
        var b = new Bag();
        Action localAct = () =>
        {
            for (int i = 0; i < 10; i++)
            {    
                lock(_locker)
                {
                    ++b.AppleNum;
                    Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} - {b.AppleNum}");
                }
                Thread.Sleep(100);
            }
        };
        Parallel.Invoke(localAct, localAct);
    }
}

In this way, the implementation results can be guaranteed, and there will be 20 apples in the bag. Of course, there are other concurrency control methods, but that is not the focus of this article.

 

 

Put apples in the bag} case demonstration 1

In some scenarios, we have another requirement. We care about how many apples each thread puts in the Bag. At this time, we need to make the Bag object related to the thread (there are multiple bags, and each Bag is owned by the thread). This requires the key content of this article - thread local variables.

Without using thread local variables, a simple way to achieve the above purpose is to put variables into the working function as internal variables of the function.

public class Test
{
    public void TryTwoThread()
    {
        Action localAct = () =>
        {
            var b = new Bag(); //Access variables to working functions
            for (int i = 0; i < 10; i++)
            {
                ++b.AppleNum;
                Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} - {b.AppleNum}");
                Thread.Sleep(100);
            }

        };
        Parallel.Invoke(localAct, localAct);
    }
}

We can see the results as we wish.

 

If our working function is independent of a class and the variables to be accessed concurrently are members of the class, the above method is not applicable.
The Action of the previous example is replaced by the following work class:

public class Worker
{
    private Bag _bag = new Bag();

    public void PutTenApple()
    {            
        for (int i = 0; i < 10; i++)
        {
            PutApple();
            Show();
            Thread.Sleep(100);
        }
    }

    private void PutApple()
    {
        ++_bag.AppleNum;
    }

    private void Show()
    {
        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} - {_bag.AppleNum}");
    }
}

The test method is changed to:

public void TryTwoThread()
{
    var worker = new Worker();
    Parallel.Invoke(worker.PutTenApple, worker.PutTenApple);
}

Note that the above Worker class is also an example that does not meet the requirements for each thread to operate its own associated variables independently. And because there is no concurrency control, the execution result of the program is uncontrollable.

We can also_ The bag variable is declared in PutTenApple to achieve the same effect as the thread local variable, but it is inevitable to pass parameters when calling PutApple and Show methods.

Here are some ways to implement thread local variables.

Putting apples in the bag -- a demonstration of a thread related static field

The first method uses threadstaticatattribute for thread related static fields. This is also a better performance method recommended by Microsoft.
The method is to declare the member variable as static and mark it with [ThreadStatic]. Based on the previous code, we make the following modifications:

[ThreadStatic] private static Bag _bag = new Bag();

Note that this implementation is problematic. It will be described in detail below.

If you also have the cosmic plug-in Resharper installed on your VS, you will see the following prompt under the code for initializing this static variable:

This tip is also available on the official website of ReSharper explain.

In short, the above initializer will only be called once, resulting in the result that only the first thread executing this method can get it correctly_ The value of the bag member, which can be accessed by subsequent processes_ Bag, you will find_ Bag is still uninitialized - null.

The solution I choose to solve this problem is to initialize in the working method_ bag variable.

 

public class Worker
{
    [ThreadStatic] private static Bag _bag;

    public void PutTenApple()
    {
        _bag = new Bag(); //Initialize before call
        for (int i = 0; i < 10; i++)
        {
            PutApple();
            Show();
            Thread.Sleep(100);
        }
    }

    private void PutApple()
    {
        ++_bag.AppleNum;
    }

    private void Show()
    {
        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} - {_bag.AppleNum}");
    }
}

The method given by ReSharper website is to wrap the static field through a property, and replace the access to the static field with the access to the static property.

public class Worker
{
    [ThreadStatic] private static Bag _bag;

    public static Bag Bag => _bag ?? (_bag = new Bag());

    public void PutTenApple()
    {
        for (int i = 0; i < 10; i++)
        {
            PutApple();
            Show();
            Thread.Sleep(100);
        }
    }

    private void PutApple()
    {
        ++Bag.AppleNum;
    }

    private void Show()
    {
        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} - {Bag.AppleNum}");
    }
}

For thread local variables, if you access them outside the thread, you will find that they are not affected by thread operation.

public void TryTwoThread()
{

    var worker = new Worker();
    Parallel.Invoke(worker.PutTenApple, worker.PutTenApple);

    Console.WriteLine($"Main Thread : {Thread.CurrentThread.ManagedThreadId} - {Worker.Bag.AppleNum}");
}

Access in main thread:

 

Case demonstration of putting apples in bags -- data Cao

Another equivalent method is to use LocalDataStoreSlot, but the performance is not as good as the ThreadStatic method described above.

public class Worker
{
    private LocalDataStoreSlot _localSlot = Thread.AllocateDataSlot();

    public void PutTenApple()
    {
        Thread.SetData(_localSlot, new Bag());

        for (int i = 0; i < 10; i++)
        {
            PutApple();
            Show();
            Thread.Sleep(100);
        }
    }

    private void PutApple()
    {
        var bag = Thread.GetData(_localSlot) as Bag;
        ++bag.AppleNum;
    }

    private void Show()
    {
        var bag = Thread.GetData(_localSlot) as Bag;
        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} - {bag.AppleNum}");
    }
}

The Thread related data is stored in the LocalDataStoreSlot object and accessed through the GetData and SetData of the Thread.

There is also a named allocation method for data slots:

private LocalDataStoreSlot _localSlot = Thread.AllocateNamedDataSlot("Apple");

public void PutTenApple()
{
    _localSlot = Thread.GetNamedDataSlot("Apple");//For demonstration
    Thread.SetData(_localSlot, new Bag());

    for (int i = 0; i < 10; i++)
    {
        PutApple();
        Show();
        Thread.Sleep(100);
    }
}

In the case of multiple components, it is useful to distinguish data slots by different names. However, if you accidentally give different components the same name, it will lead to data pollution.
The performance of the data slot is low, and Microsoft does not recommend it. Moreover, it is not a strong type, and it is not very convenient to use.

.NET 4 - ThreadLocal

Yes NET Framework 4 adds a generic local variable storage mechanism ThreadLocal < T >. The following example is also modified based on the previous example. The use of ThreadLocal < T > is well understood compared with the previous code. The constructor of ThreadLocal < T > receives a lambda for delayed initialization of thread local variables, and the Value of local variables can be accessed through the Value attribute. IsValueCreated determines whether the local variable has been created.

public class Worker
{
    private ThreadLocal<Bag> _bagLocal = new ThreadLocal<Bag>(()=> new Bag(), true);

    public ThreadLocal<Bag> BagLocal => _bagLocal;

    public void PutTenApple()
    {
        if (_bagLocal.IsValueCreated) //Thread local variables are not created until the first access
        {
            Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} - Initialized");
        } 

        for (int i = 0; i < 10; i++)
        {
            PutApple();
            Show();
            Thread.Sleep(100);
        }

        if (_bagLocal.IsValueCreated)
        {
            Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} - Initialized");
        }
    }

    private void PutApple()
    {
        var bag = _bagLocal.Value; //adopt Value Property access
        ++bag.AppleNum;
    }

    private void Show()
    {
        var bag = _bagLocal.Value; //adopt Value Property access
        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} - {bag.AppleNum}");
    }
}

In addition, if trackAllValues is set to true when initializing ThreadLocal < T >, the value stored in the thread local variable can be accessed outside the thread using ThreadLocal < T >. As in the test code:

public void TryTwoThread()
{
    var worker = new Worker();
    Parallel.Invoke(worker.PutTenApple, worker.PutTenApple);

    // have access to Values Access all thread local variables outside the thread (required) ThreadLocal On initialization trackAllValues Set as true)
    foreach (var tval in worker.BagLocal.Values)
    {
        Console.WriteLine(tval.AppleNum);
    }
}

Reprinted from: https://www.cnblogs.com/lsxqw2004/p/6121889.html

 

Topics: C#