【问题标题】:Disposing thread static variable处理线程静态变量
【发布时间】:2019-10-20 00:44:51
【问题描述】:

我在静态类中有一个ThreadStatic 成员。静态类用于多线程环境。我想确保当线程返回到线程池(或重新使用)时,成员被释放(或重新初始化),因此特定线程的任何后续使用都会获得变量的新副本。该成员必须保持静态,因此实例成员不会真正提供帮助。

我尝试过使用ThreadLocalAsyncLocalCallContext,但这些都没有真正的帮助。 (CallContext 主要用于概念验证,它是一个 .net 标准应用程序,因此 callcontext 无论如何都不起作用)。

这只是我编写的一个示例代码,用于重现我的问题,有ThreadStaticThreadLocalAsyncLocalCallContext 进行测试。


    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}");
            }

        }
    }

上面的代码是在一个线程中运行的,所以线程可以被重用。

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:

但是,从输出中可以看出,当进行第二次调用并重用线程 1 时,它仍然具有线程 0 设置的值。

线程重用时,有什么办法可以将ThreadStatic变量重置为默认值或null?

【问题讨论】:

  • ThreadStatic 和线程池是一个非常糟糕的组合。 AsyncLocal 是专门为解决这个问题而发明的。
  • @HansPassant 是的,我意识到这一点,但 AsyncLocal 也没有解决我的问题..
  • 很难说,我觉得还可以。您不能假设静态构造函数在任何特定线程上运行。
  • @Tanzeel - 我相信你在滥用AsyncLocal,在async 流之外使用它。混合static AsyncLocalParallel Task Library 是未定义的,AFAIK。如果您希望在重新使用线程时将静态值设置为新值,则需要编写代码来执行此操作。一个干净的方法是定义一个class MyContext,它包含您需要的所有信息。然后你可以只有一个静态:[ThreadStatic] static MyContext myContext; 你仍然需要在停止使用线程时手动将其设置为 null - 但至少这样做变得更易于管理。

标签: c# multithreading thread-local threadstatic callcontext


【解决方案1】:

TL;DR

如果不希望多线程应用程序中的多个线程重用变量,则没有理由将其设为静态。

如果我们不希望同一个线程重用一个变量,那么为什么我们会故意使用[ThreadStatic] 是值得怀疑的,因为它允许我们这样做。


我专注于 ThreadStatic 方面,因为它似乎是问题的焦点。

因此对特定线程的任何后续使用都会获得该变量的新副本。

线程的使用不需要他们自己的变量副本 - 使用变量的方法可能需要也可能不需要他们自己的变量副本。这听起来像是一件令人毛骨悚然的事情,但线程本身并不需要任何变量的副本。它可能正在做与这个静态类和这个变量无关的事情。

当我们使用变量时,我们才关心它是否是“新副本”。也就是说,当我们调用使用该变量的方法时。

如果,当我们使用静态变量(在方法之外声明)时,我们想要确保它在我们使用它之前是新实例化的,并在我们用完它时被释放,那么我们可以做到这一点在使用它的方法中。我们可以实例化它,释放它,如果我们愿意的话,甚至可以将它设置为null。然而,当我们这样做时,显而易见的是,它通常消除了在使用它的方法之外声明变量的任何需要。

如果我们这样做:

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

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

我们已经实现了目标。

线程重用时,有什么办法可以将 ThreadStatic 变量重置为默认值或 null?

完成。每次同一个线程进入方法(“线程被重新使用”)它都是空的。

但如果这就是我们想要的,那为什么不直接这样做呢?

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

结果完全一样。每个线程都以DisposableThing 的新实例开始,因为当它执行该方法时,它会声明变量并创建一个新实例。而不是将其设置为 null 引用超出范围。

两者之间的唯一区别是,在第一个示例中,DisposableThing 在课堂之外公开暴露。这意味着其他线程可以使用它而不是声明自己的变量,这很奇怪。既然他们还需要确保在使用它之前对其进行实例化,那么他们为什么不也像第二个示例那样创建自己的实例呢?

确保在静态方法中每次需要变量时都对其进行初始化和处置的最简单和最常规的方法是在静态方法中本地声明该变量并创建一个新实例。然后无论有多少线程同时调用它,它们都将使用一个单独的实例。

【讨论】:

  • 没错,但这不是导致需要静态变量的使用模式。所以,与问题无关。 (除了提醒编码人员静态变量是一种全局状态,而且应该始终问自己“这真的需要全局/静态吗?”)减少“全局状态”的一个好模式是设计一个“ class MyContext”,并传递一个包含所有需要信息的“上下文”对象。
  • 我同意。然而它类似于问题中的使用模式。问题是如何正确使用这个静态变量。在这种情况下,答案是他们不需要一个。一旦你知道它就很明显,但我认为它会使新开发者感到困惑。也许他们没有创建课程——他们只是在维护它——甚至没有意识到改变范围是一种选择,
  • 很公平 - 我同意问题中显示的代码可以修改为不需要静态变量,如您所示 - 如果可能的话,这是更好的解决方案。 (但我假设 OP 有一种情况,可以方便地使用静态。也就是说,多个方法需要访问相同的值。你的答案当然没有解决。)跨度>
猜你喜欢
  • 2011-07-17
  • 1970-01-01
  • 2012-03-15
  • 2013-03-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-11-18
相关资源
最近更新 更多