4

I have a ThreadStatic member in a static class. The static class is used in a multi threaded environment. I want to make sure that when a thread is returned to threadpool (or re-used), the member is disposed (or re-initialized), so any subsequent uses of the particular thread gets a fresh copy of the variable. The member has to stay static so an instance member will not really help.

I have tried using ThreadLocal, AsyncLocal and CallContext but none of these really help. (CallContext is mostly for proof of concept, its a .net standard app so callcontext won't work anyways).

This is just a sample code I wrote to recreate my problem having ThreadStatic, ThreadLocal, AsyncLocal and CallContext for testing.


    class Program
    {
        static void Main(string[] args)
        {
            var act = new List<Action<int>>()
            {
                v=> ThreadClass.Write(v),
                v=> ThreadClass.Write(v),
            };

            Parallel.ForEach(act, new ParallelOptions { MaxDegreeOfParallelism = 1 }, (val, _, index) => val((int)index));

            Console.WriteLine($"Main: ThreadId: {Thread.CurrentThread.ManagedThreadId} ThreadStatic = {ThreadClass.ThreadStatic} ThreadLocal = {ThreadClass.ThreadLocal.Value} AsyncLocal = {ThreadClass.AsyncLocal.Value} CallContext: {ThreadClass.CallContextData}");

            Console.ReadKey();
        }
    }

    public static class ThreadClass
    {
        static object _lock = new object();

        [ThreadStatic]
        public static string ThreadStatic;

        public static ThreadLocal<string> ThreadLocal = new ThreadLocal<string>(() => "default");


        public static readonly AsyncLocal<string> AsyncLocal = new AsyncLocal<string>();

        public static string CallContextData
        {
            get => CallContext.LogicalGetData("value") as string;
            set => CallContext.LogicalSetData("value", value);
        }

        static ThreadClass()
        {
            AsyncLocal.Value = "default";
        }


        public static void Write(int id)
        {
            lock (_lock)
            {
                Console.WriteLine($"{id} Init: ThreadId: {Thread.CurrentThread.ManagedThreadId} ThreadStatic = {ThreadStatic} ThreadLocal = {ThreadLocal.Value} AsyncLocal = {AsyncLocal.Value} CallContext: {ThreadClass.CallContextData}");

                ThreadStatic = $"Static({id})";
                ThreadLocal.Value = $"Local({id})";
                AsyncLocal.Value = $"Async({id})";
                CallContextData = $"Call({id})";

                Console.WriteLine($"{id} Chng: ThreadId: {Thread.CurrentThread.ManagedThreadId} ThreadStatic = {ThreadStatic} ThreadLocal = {ThreadLocal.Value} AsyncLocal = {AsyncLocal.Value} CallContext: {ThreadClass.CallContextData}");
            }

        }
    }

The above code is run in a single thread so the thread can be re-used.

0 Init: ThreadId: 1 ThreadStatic =  ThreadLocal = default AsyncLocal = default CallContext:
0 Chng: ThreadId: 1 ThreadStatic = Static(0) ThreadLocal = Local(0) AsyncLocal = Async(0) CallContext: Call(0)
--------------------
1 Init: ThreadId: 1 ThreadStatic = Static(0) ThreadLocal = Local(0) AsyncLocal = Async(0) CallContext: Call(0)
1 Chng: ThreadId: 1 ThreadStatic = Static(1) ThreadLocal = Local(1) AsyncLocal = Async(1) CallContext: Call(1)
--------------------
Main: ThreadId: 1 ThreadStatic = Static(1) ThreadLocal = Local(1) AsyncLocal =  CallContext:

However, as seen in the output, when the second call is made and thread 1 is reused, it still has the values set by thread 0.

Is there any way to reset ThreadStatic variable to default value or null when thread is re used?

Kevin
  • 16,549
  • 8
  • 60
  • 74
Tanzeel
  • 51
  • 4
  • 2
    ThreadStatic and the thread pool are a very poor mix. AsyncLocal was specifically invented to deal with this problem. – Hans Passant Jun 04 '19 at 14:04
  • @HansPassant yes, I realize that, but AsyncLocal is not solving my problem either .. – Tanzeel Jun 04 '19 at 14:11
  • Hard to tell, looks okay to me. You must not assume that a static constructor runs on any particular thread. – Hans Passant Jun 04 '19 at 14:16
  • @Tanzeel - I believe you are abusing `AsyncLocal` by using it outside of an `async` flow. Mixing `static AsyncLocal` and `Parallel Task Library` is undefined, AFAIK. If you want statics set to a fresh value when a thread is re-used, you'll need to write the code to do so. A clean approach is to define a `class MyContext` that holds all the info you need. Then you could have just one static: `[ThreadStatic] static MyContext myContext;` You'll still need to manually set it to null whenever you stop using a thread - but at least doing so becomes more manageable. – ToolmakerSteve Aug 06 '20 at 00:06

2 Answers2

1

TL;DR

If don't want a variable reused by multiple threads in a multi-threaded application, there's no reason to make it static.

If we don't want a variable reused by the same thread, it's questionable why we would deliberately use [ThreadStatic], since that's what it allows us to do.


I'm focusing on the ThreadStatic aspect of this since it seems to be a focus of the question.

so any subsequent uses of the particular thread gets a fresh copy of the variable.

Uses of the thread don't need their own copy of the variable - methods that use the variable may or may not need their own copy of the variable. That sounds like a hair-splitting thing to say, but a thread, by itself, doesn't need a copy of any variable. It could be doing things unrelated to this static class and this variable.

It's when we use the variable that we care whether it is a "fresh copy." That is, when we invoke a method that uses the variable.

If, when we use the static variable (which is declared outside of a method), what we want is to ensure that it's newly instantiated before we use it and disposed when we're done with it, then we can accomplish that within the method that uses it. We can instantiate it, dispose it, even set it to null if we want to. What becomes apparent as we do this, however, is that it usually eliminates any need for the variable to be declared outside the method that uses it.

If we do this:

public static class HasDisposableThreadStaticThing
{
    [ThreadStatic]
    public static DisposableThing Foo;

    public static void UseDisposableThing()
    {
        try
        {
            using (Foo = new DisposableThing())
            {
                Foo.DoSomething();
            }
        }
        finally
        {
            Foo = null;
        }
    }
}

We've accomplished the goal.

Is there any way to reset ThreadStatic variable to default value or null when thread is re used?

Done. Every time the same thread enters the method ("the thread is re used") it's null.

But if that's what we want, then why not just do this?

public static class HasDisposableThreadStaticThing
{
    public static void UseDisposableThing()
    {
        using (var foo = new DisposableThing())
        {
            foo.DoSomething();
        }
    }
}

The result is exactly the same. Every thread starts with a new instance of DisposableThing because when it executes the method it declares the variable and creates a new instance. Instead of setting it to null the reference goes out of scope.

The only difference between the two is that in the first example, DisposableThing is publicly exposed outside of the class. That means that other threads could use it instead of declaring their own variable, which is weird. Since they would also need to make sure it's instantiated before using it, why wouldn't they also just create their own instance as in the second example?

The easiest and most normal way to ensure that a variable is initialized and disposed every time it's needed in a static method is to declare that variable locally within the static method and create a new instance. Then regardless of how many threads call it concurrently they will each use a separate instance.

Scott Hannen
  • 27,588
  • 3
  • 45
  • 62
  • True, but this isn't the usage-pattern that leads to needing a static variable. So, irrelevant to the question. (Other than reminding coders that a static variable is a type of global state, and one should always ask oneself "Does this really need to be a global/static?") A good pattern for reducing "global state" is to design a "class MyContext", and pass around a "context" object that has all needed info. – ToolmakerSteve Aug 05 '20 at 23:33
  • I agree. And yet It's similar to the usage pattern in the question. The question was how to correctly use this static variable. The answer in this case was that they didn't need one. It's obvious once you know it, but I think it can confuse newer devs. Maybe they didn't make the class - they're just maintaining it - and didn't even realize that changing the scope was an option, – Scott Hannen Aug 06 '20 at 02:49
  • Fair enough - I agree that the code shown in the question can be reworked to not need a static variable, as you show - and that is the better solution when possible. (But I assume that OP had a situation where it was convenient to have a static. That is, **multiple methods** needed access to the same value. Which your answer of course doesn't address.) – ToolmakerSteve Aug 06 '20 at 20:31
0

Unfortunately, ThreadPool does not provide an API to listen to repool events to do this universally. However, if you have control over every place that queues work to the ThreadPool, you can write a simple wrapper to do what you want.

public struct DisposableThreadStatic<T> : IDisposable where T : class, IDisposable
{
    [ThreadStatic]
    private static T ts_value;

    private bool _shouldDispose;

    public T Value => ts_value;

    public static DisposableThreadStatic<T> GetOrCreate(Func<T> creator)
    {
        if (ts_value == null)
        {
            ts_value = creator();
            return new DisposableThreadStatic<T>() { _shouldDispose = true };
        }
        return default;
    }

    public void Dispose()
    {
        if (_shouldDispose && ts_value != null)
        {
            ts_value.Dispose();
            ts_value = null;
        }
    }
}

With this, you can wrap your threaded function with this.

ThreadPool.QueueUserWorkItem(_ =>
{
    using var dts = DisposableThreadStatic<MyDisposable>.GetOrCreate(() => new MyDisposable());
    // Use value, call any other functions, etc.
    dts.Value.Func();
});

And using that same GetOrCreate call anywhere deeper in the call stack will just return the cached value, and only the top-most call (when the work completes) will dispose it.

Tim
  • 91
  • 9