线程重命名

时间:2010-07-28 13:34:02

标签: c# .net multithreading

在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)根本不使用。

你知道为什么Thread重命名是一次写入操作吗?如果更改名称有什么想法会破坏什么?

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

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来指定哪个线程执行哪个操作。提前谢谢。

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

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

10 个答案:

答案 0 :(得分:10)

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

答案 1 :(得分:6)

我使用了来自Reflector的分析操作,我看到的BCL中的唯一代码(或者更确切地说是Nikolaos看到的)使用Thread.Name getter的代码是对RegisterClassEx的调用user32.dll中的API。 Thread类本身仅指m_Name getter和setter中的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});
}

需要注意的另一件事是,您可能会遇到代码访问安全性问题以及根据应用程序具有的信任级别更改私有成员。显然,你的测试似乎没有发挥作用,但这里值得一提。

答案 2 :(得分:4)

我不会以你的方式更改线程的名称。虽然您正在执行写入多次操作所需的相同操作,但您不知道没有行为依赖于该操作是一次写入。

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

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

如果要捕获某个语义,则不应该使记录器和线程符合将捕获该语义的模式。相反,在特定时间点明确指示记录器的语义。

答案 3 :(得分:2)

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

答案 4 :(得分:1)

.NET(和Java)中的线程名称仅用于调试和诊断目的。虽然因为Java可以重命名其.NET可以做同样的线程的逻辑是错误的(因为.NET线程是具有附加功能的系统线程的包装器,就像Java线程一样,但它们是不相关的),更改线程名称没有任何伤害本身,除了因为您使用非公共API而在未来版本中存在破坏的风险。

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

答案 5 :(得分:1)

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

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

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

答案 6 :(得分:1)

我今天点击这个,就像我的代码即将投入生产一样:-(我无法理解这个设计决定的原因。

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

答案 7 :(得分:0)

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

我很想知道是否有一个丢失的具体例子,因为我也使用log4net,看看你在驾驶什么。 :)

<强>更新

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

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

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

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

  4. 这会重新命名线程并处理它们重新命名它们,这样如果没有命名线程的东西注销它就不会以不正确的名字注销。

    更新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...
    }
    

答案 8 :(得分:0)

更改名称或尝试更改名称可能会破坏某些内容。如果System.Threading.Thread的实现发生更改,以便字段m_Name被称为m_ThreadName,例如,在.NET Framework的未来版本中,或者确实是Service Pack或hot-fix(尽管可能不太可能),但您的代码将抛出异常。

答案 9 :(得分:0)

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