【问题标题】:Thread Renaming线程重命名
【发布时间】:2010-07-28 13:34:02
【问题描述】:

在 Java 中,重命名线程是可能的。在.NET 中不是。这是因为 Name 是 Thread 类中只写一次的属性:

public string Name
{
    get
    {
        return this.m_Name;
    }
    [HostProtection(SecurityAction.LinkDemand, ExternalThreading=true)]
    set
    {
        lock (this)
        {
            if (this.m_Name != null)
            {
                throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_WriteOnce"));
            }
            this.m_Name = value;
            InformThreadNameChangeEx(this, this.m_Name);
        }
    }
}

鉴于 Java 允许重命名线程,并且使用的大多数底层线程结构在两个平台上都是由操作系统提供的,我倾向于认为我实际上可以在 C# 中重命名一个线程,如果我避免使用某组a) 我不在乎或 b) 根本不使用的功能。

你知道为什么线程重命名是一次写入操作吗?知道更改名称是否会破坏某些内容吗?

我尝试了一个测试,我将线程重命名为:

var t1 = new Thread(TestMethod);
t1.Name = "abc";
t1.Start();
t1.GetType().GetField("m_Name", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(t1, "def");
t1.GetType().GetMethod("InformThreadNameChangeEx", BindingFlags.NonPublic | BindingFlags.Static).Invoke(t1, new object[] { t1, t1.Name});

结果是名称确实发生了变化,这反映在使用该线程的其他代码上。这样做的背景是我需要记录线程所做的事情,而我使用的日志库(log4net)使用 Thread.Name 来指定哪个线程执行什么操作。提前致谢。

编辑:请停止建议明显的事情!如果我问如何重新命名线程,我知道如何在开始时命名线程。

我需要这样做的原因是线程将被重新使用,并且它可能被另一个组件使用,我想表明这一点,如果以及何时会发生日志记录,以便有一个特定的线程名称,而不是通用编号。

【问题讨论】:

  • 为什么要重命名线程?不是因为Java可以做到吗?
  • @ChrisF:请参阅上面的编辑。
  • 很公平,尽管您可以从Thread 派生一个LoggingThread,它公开一个名为ThreadName 的读/写属性并使用它来代替。我知道这是一个具有相同值的额外属性,但它可以满足您的需求,并且不依赖可能会中断的反射。
  • @Chris:线程是一个密封类,所以我不能使用继承来覆盖名称。此外,正如我上面提到的,log4net 使用 Thread.Name,所以我也不能使用组合,如引入 ThreadName 属性,因为它永远不会被 log4net 读取。
  • 积分。在发布这样的 cmets 之前,我应该仔细检查。干得好,我没有把它作为答案发布;)

标签: c# .net multithreading


【解决方案1】:

操作系统级别的线程没有名称。真的,这只是一个方便的功能。

【讨论】:

【解决方案2】:

我使用了来自Reflector 的分析操作,并且我看到的(或者更准确地说是 Nikolaos 看到的)BCL 中唯一使用Thread.Name getter 的代码是对 user32.dll 中的RegisterClassEx API 的调用。 Thread 类本身仅引用 Name getter 和 setter 中的 m_Name 成员。我怀疑以您采用的方式重命名线程是安全的。除了我会更改您的代码以获取与Thread.Name 相同的对象的锁定。幸运的是,这正是 Thread 实例本身,所以很容易做到。

var t1 = new Thread(TestMethod); 
t1.Name = "abc"; 
t1.Start(); 
lock (t1) 
{
  t1.GetType().
      GetField("m_Name", BindingFlags.Instance | BindingFlags.NonPublic).
      SetValue(t1, "def"); 
  t1.GetType().
      GetMethod("InformThreadNameChangeEx", BindingFlags.NonPublic | 
          BindingFlags.Static).
      Invoke(t1, new object[] { t1, t1.Name});
}

另一件需要注意的事情是,您可能会遇到代码访问安全性和更改私有成员的问题,具体取决于应用程序的信任级别。显然,这似乎与您的测试无关,但在这里值得一提。

【讨论】:

  • 我也进行了分析,结果表明 MS.Win32.HwndWrapper..ctor(...) 使用了 Thread.Name getter。它使用它来设置新窗口的“字符串 lpszClassName”...
  • @Nikolaos:是的,不错的收获。我不知道我第一次是怎么错过的。
  • InformThreadNameChangeEx() 在 .NET 框架 4.0 上不可用。请参阅下面的stackoverflow.com/a/18823380/1418147
【解决方案3】:

我不会像你那样更改线程的名称。虽然您正在执行多次写入操作所需的相同操作,但您不知道不存在依赖于一次写入操作的行为。

例如,调试器如果获得线程名称,可以缓存该名称,而不必再为它调用对象。

这里还有一个设计问题,为什么要依赖线程名称来帮助进行日志记录;您依赖记录器的行为来指示您尝试记录的部分操作。

如果你想捕捉某种语义,你不应该将记录器和线程塑造成符合捕捉该语义的模式。相反,在特定时间点向记录器明确指示语义。

【讨论】:

  • 我很清楚(并且我已经尝试在上面的问题中解释这一点),这是开发 System.Threading 的程序员的一个指标,最好不要篡改。但是有谁知道这取决于什么?
  • @_NT:这就像问什么依赖于 GetType()。它是一种类型的公开可用属性,几乎不可能说什么取决于它或做出什么假设,因为它是一次性写入的。
  • @_NT:我做到了,不幸的是,这是错误的。您假设 BCL 是 Thread 类上 Name 属性的唯一使用者,这是不正确的假设;它是可公开访问类型的公共属性。一个很好的例子是 VS.NET 调试窗口中的 Thread 下拉菜单,它使用 Thread.Name 属性,但执行此操作的程序集不在 BCL 中。您还针对实现而不是合同进行编码。物业合同上写的是一次写入,你应该遵守,否则后果自负。
  • @casperOne:某处的某些代码可能依赖于设置名称并锁定它的能力,或者可能依赖于这样一个事实,即如果观察到一个线程具有某个特定的“特殊”名称它会一直这样做。不过,我真的对一次写入的设计感到困惑。一旦线程启动,我可以看到一个不可变的名称(是否设置)以及线程可以随时更新的 CurrentActionDescription 的价值(这样的设计会非常好对于线程池线程之类的东西),但我很好奇一次性写入的预期用途是什么?
  • @supercat 一次写入基本上没有以正确的方式实现不变性。如果有人缓存了一个获取/设置的值,那么处理它是一件很痛苦的事情。
【解决方案4】:

InformThreadNameChangeEx() 在 .NET Framework 4.0 上不可用(但 InformThreadNameChange() 可用)。

所以更通用的解决方案是

var t1 = new Thread(TestMethod);
t1.Name = "abc";
t1.Start();
lock (t1)
{
    t1.GetType().
        GetField("m_Name", BindingFlags.Instance | BindingFlags.NonPublic).
        SetValue(t1, null);
    t1.Name = "def";
}

【讨论】:

    【解决方案5】:

    .NET(和 Java)中的线程名称纯粹用于调试和诊断目的。虽然由于 Java 可以重命名其线程而 .NET 可以执行相同操作的逻辑是错误的(因为 .NET 线程是具有附加功能的系统线程的包装器,Java 线程也是如此,但它们在其他方面无关)​​,更改线程的名称​​本身并没有什么害处,除了冒着在未来版本中损坏的风险之外,因为您使用的是非公共 API。

    但是,你有什么理由改变它?我认为它是只读的,以避免创建执行各种任务的“厨房水槽”线程。当然,虽然也有例外,但我会提醒您考虑需要这样做的设计是否是正确的设计。

    【讨论】:

    • 是的,他们最好添加一个线程本地标签或其他东西。
    • @Adam:这不是我的设计决定,log4net 使用 Thread.Name 来记录当前线程的名称。因此,如果我想记录一个有意义的名称,我必须重命名线程,因为线程在不同的组件中重复使用,我想指明哪个。
    • @_NT:log4net 是开源的。如果它做了一些不方便的事情,请修复它。
    • @_NT:线程是如何“重用”的?一个线程用一个函数初始化,名字可以在那个时候设置,如你所知。您需要更清楚地了解究竟是什么构成了“重用”线程。
    • @Steven:我意识到这一点,但我认为他们不会接受我的更改,因为它现在适用于 99% 的代码。拥有一个私有分支意味着合并每个新版本,这可能需要相当多的工作。
    【解决方案6】:

    一种可能的解决方法是让一个实例类包含 thead ID 字典 - “名称”键值对。

    您的记录器需要返工,但可以调用字典将“名称”插入到日志语句中。

    我遇到了同样的问题,因为我正在使用刚刚被重用的线程池线程。

    【讨论】:

      【解决方案7】:

      我今天遇到了这个问题,就在我的代码即将投入生产时:-(我无法理解做出这个设计决定的原因。

      我的解决方法是将线程名称推送到log4net NDC context stack 并使用%ndc 模式记录它。如果您的某些线程不会设置 NDC,那么this answer 也很有用。

      【讨论】:

        【解决方案8】:

        更改名称或尝试更改名称可能会破坏某些内容。如果 System.Threading.Thread 的实现发生变化,例如,在 .NET Framework 的未来版本中,或者确实是服务包或修补程序(尽管可能不太可能)中,字段 m_Name 被称为 m_ThreadName ,您的代码将引发异常。

        【讨论】:

        • 确实,但我不打算使用上述方法,我只是在检查它是否可以正常工作而不会立即失败:它确实在这个-非常-实验级别上工作。
        【解决方案9】:

        某些东西很可能依赖于名称是不可变的,这是设计所要求的。通过破坏这种封装,您冒着破坏(真诚地)依赖于此的东西的风险。我不能举一个具体的例子,但由于这个属性可以以任何可以想象的方式使用,所以问题是无限的。作为一个理论示例,可以使用线程的名称作为集合中的键。

        我很想知道是否有一个死板的具体例子,因为我也使用 log4net 并看看你在做什么。 :)

        更新

        这无疑激起了我作为 log4net 用户的兴趣。不想维护 log4net 的分支,这是一个更安全的可能解决方案。

        1. 为 log4net 编写一个包装器,即一个 ILog 类型的接口(我已经有一个 15 分钟的工作)。

        2. 使用线程局部变量技术在组件的入口点记录线程名称(例如,使用扩展方法 Thread.LoggingName = "blah blah blah")。

        3. 在您的日志记录包装器中临时更改线程名称,然后在记录后再次将其更改回来。

        这会处理重命名线程并再次处理重命名它们,这样如果没有命名线程的东西注销,它就不会以错误的名称注销。

        更新 2

        该技术的粗略示例:

        public class MyComponent
        {
            public void EntryPoint()
            {
                MyLogger.CurrentLoggerThreadName = "A thread contextual name.";
        
                _myLogger.Info("a logging message.");
        
                SomeOtherMethod();
            }
        
            private void SomeOtherMethod()
            {
                _myLogger.Info("another logging message with the same thread name.");
            }
        }
        
        public class MyLogger
        {
            [ThreadStatic]
            private static string _CurrentLoggerThreadName;
        
            private static readonly FieldInfo NameField = typeof(Thread).GetType().GetField("m_Name", BindingFlags.Instance | BindingFlags.NonPublic);
        
            public static string CurrentLoggerThreadName
            {
                get { return _CurrentLoggerThreadName; }
                set { _CurrentLoggerThreadName = value; }
            }
        
            private static void LogWithThreadRename(Action loggerAction)
            {
                Thread t1 = Thread.CurrentThread;
        
                string originalName = (string)NameField.GetValue(t1);
        
                try
                {
                    NameField.SetValue(t1, CurrentLoggerThreadName);
                    loggerAction();
                }
                finally
                {
                    NameField.SetValue(t1, originalName);
                }
            }
        
            public void Info(object message)
            {
                LogWithThreadRename(() => _iLog.Info(message));
            }
        
            //More logging methods...
        }
        

        【讨论】:

        • 我意识到,正如您从从事 System.Threading 的程序员那里得到的一样,这清楚地表明了命名 if(根据他们的理解)非常重要。但我想知道是否有人确切知道这取决于什么? (单声道开发者我在看你)
        • @_NT 我正在考虑的不仅仅是框架的东西。它是一个公共财产,所以任何东西都可以依赖它,例如您使用的其他框架和工具。
        • 非常有趣的更新。您能否详细说明第 2 点,您想到的线程局部变量技术是什么。在 3 中,您的意思是仅更改支持字段“m_Name”(即不调用本机通知方法)然后将其设置回来?最后一个听起来很无害,尽管依赖于字段名称。
        • @_NT 请查看我的更新。是的,我建议只更改名称,然后再将其更改回来。这应该足以让 log4net 注销所需的日志记录线程名称。
        【解决方案10】:

        vinzbe 的上述回答是我认为有用的。 Brain Gideon 的回答我遇到的问题是 InformThreadNameChange (.net 4.0) 需要 ThreadHandle 结构。因此,仅执行上述操作不会通知 VS 发生名称更改,但是您可以在我包含的代码中看到,将 Name 设置为 null 后,将胎面名称设置为 null 它将传播。

        感谢大家的帮助

        /// <summary>
        /// Class ThreadName.
        /// </summary>
        public class ThreadName
        {
            /// <summary>
            /// Updates the name of the thread.
            /// </summary>
            /// <param name="strName" type="System.String">Name of the string.</param>
            /// <param name="paramObjects" type="System.Object[]">The parameter objects.</param>
            /// <remarks>if strName is null, just reset the name do not assign a new one</remarks>
            static public void UpdateThreadName(string strName, params object[] paramObjects)
            {
                //
                // if we already have a name reset it
                //
                if(null != Thread.CurrentThread.Name)
                {
                    ResetThreadName(Thread.CurrentThread);                
                }
        
                if(null != strName)
                {
                    StringBuilder   sbVar   = new StringBuilder();
                    sbVar.AppendFormat(strName, paramObjects);
                    sbVar.AppendFormat("_{0}", DateTime.Now.ToString("yyyyMMdd-HH:mm:ss:ffff"));
                    Thread.CurrentThread.Name = sbVar.ToString();
                }
            }
        
            /// <summary>
            /// Reset the name of the set thread.
            /// </summary>
            /// <param name="thread" type="Thread">The thread.</param>
            /// <exception cref="System.NullReferenceException">Thread cannot be null</exception>
            static private void ResetThreadName(Thread thread)
            {
                if(null == thread) throw new System.NullReferenceException("Thread cannot be null");
                lock(thread)
                {
                    //
                    // This is a private member of Thread, if they ever change the name this will not work
                    //
                    var field = thread.GetType().GetField("m_Name", BindingFlags.Instance | BindingFlags.NonPublic);
                    if(null != field)
                    {
                        //
                        // Change the Name to null (nothing)
                        //
                        field.SetValue(thread, null);
        
                        //
                        // This 'extra' null set notifies Visual Studio about the change
                        //
                        thread.Name = null;
                    }
                } 
            }
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2010-10-02
          • 2011-08-10
          • 1970-01-01
          • 1970-01-01
          • 2011-12-02
          • 1970-01-01
          • 1970-01-01
          • 2011-03-21
          相关资源
          最近更新 更多