Переименование темы

В Java возможно переименование потоков. В .NET это не так. Это связано с тем, что Имя является свойством, которое однократно записывается в классе 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 разрешает переименование потоков и большинство используемых структур нитей, поставляемых ОС на обеих платформах, я склонен думать, что могу фактически переименовать поток на С#, если я избегу определенного набора функциональность, которая: а) мне все равно или б) вообще не использовать.

Есть ли у вас какие-либо идеи, почему переименование потоков является операцией однократной записи? Любая идея, если изменение названия что-то сломает?

Я пробовал тест, где я переименовал поток как таковой:

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, чтобы указать, какой поток выполняет какое действие. Спасибо заранее.

EDIT: Пожалуйста, прекратите предлагать очевидные вещи! Я знаю, как назвать поток при запуске, если я спрашиваю, как RE-name.

Причина, по которой мне нужно это сделать, заключается в том, что поток будет повторно использован и может использоваться другим компонентом, и я хочу обозначить это, если и когда будет происходить ведение журнала, чтобы иметь конкретное имя потока, а не общее число.

Ответы

Ответ 1

Я использовал операцию анализа из Reflector и единственный код в BCL, который я видел (точнее, Nikolaos), который использует геттер Thread.Name был вызовом API RegisterClassEx в user32.dll. Сам класс 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

Темы, на уровне ОС, не имеют имен. Действительно, это просто удобная функция.

Ответ 3

Я бы не стал менять название потока так, как вы. В то время как вы делаете то же самое, что требует операция записи-много, вы не знаете, что нет действий, зависящих от того, что операция является однократной записью.

Например, отладчик, если он получает имя потока, может кэшировать это имя и не должен вызывать объект для него больше.

Здесь также возникает вопрос о дизайне, почему вы зависите от имени потока, которое поможет в вашем протоколировании; вы полагаетесь на поведение регистратора, чтобы указать часть операции, которую вы пытаетесь выполнить.

Если вы хотите захватить определенную семантику, вам не следует формировать логгер и поток, чтобы они соответствовали шаблону, который будет захватывать эту семантику. Вместо этого явным образом указываю семантику регистратору в определенный момент времени.

Ответ 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.

Однако, какая у вас причина для его изменения? Я думаю, что это было сделано только для чтения, чтобы избежать создания потоков "кухонных раковин", которые выполняют всевозможные задачи. Конечно, есть исключения, я бы посоветовал вам подумать над тем, нужен ли дизайн, требующий этого, правильного дизайна.

Ответ 6

Возможная работа вокруг будет состоять в том, чтобы иметь класс экземпляра, содержащий словарь из пары значений id ID - "name".

Ваш регистратор нуждается в переделке, но словарь можно вызвать, чтобы вставить "имя" в оператор журнала.

У меня такая же проблема, поскольку я использую потоки пула потоков, которые просто используются повторно.

Ответ 7

Я ударил этого сегодня, точно так же, как мой код собирался выйти на производство:-( и я не могу понять причину этого дизайнерского решения.

Мое исправление заключалось в том, чтобы направить имя потока в log4net NDC стек контекста и занести его в журнал с помощью шаблона %ndc. Если некоторые из ваших потоков не будут устанавливать NDC, тогда этот ответ также будет полезен.

Ответ 8

Существует определенная возможность того, что что-то полагается на неизменное имя, что и требует мандат. Разрушая эту инкапсуляцию, вы рискуете сломать что-то, что (добросовестно) полагается на это. Я не могу привести конкретный пример, но поскольку это свойство можно использовать любым возможным способом, проблема неограничена. В качестве теоретического примера что-то может использовать имя потока в качестве ключа в коллекции.

Мне бы хотелось знать, был ли конкретный пример с капелькой, хотя я тоже использую log4net и вижу, во что вы едете.:)

Обновление

Это, безусловно, вызвало мой интерес как пользователя log4net. Не желая поддерживать вилку log4net, это возможное решение, которое безопаснее.

  • Напишите оболочку для log4net, то есть интерфейс типа ILog (у меня уже есть одна и 15 минут работы).

  • Используйте метод локальной локальной переменной для записи имени потока (например, с помощью метода расширения Thread.LoggingName = "blah blah blah" ) в точках входа в ваши компоненты.

  • В вашей логгеровой обертке временно измените имя потока и затем снова измените его после ведения журнала.

Это позаботится о том, чтобы переименовать потоки и обработать повторное присвоение им имени снова, так что если что-то, что не выдает нити, выйдет из системы, оно не выйдет с неправильным именем.

Обновление 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...
}

Ответ 9

Изменение имени или попытка изменить имя может сильно нарушить что-то. Если реализация System.Threading.Thread изменяется так, что поле m_Name называется m_ThreadName, например, в будущей версии .NET Framework или действительно пакете обновления или исправлении (маловероятно, хотя это может быть), ваш код выдаст исключение.

Ответ 10

Вышеупомянутый ответ vinzbe был тем, что я нашел полезным. Ответ от Brain Gideon У меня возникла проблема в том, что структура ThreadHandle требуется для InformThreadNameChange (.net 4.0). Таким образом, просто выполнение выше не будет информировать VS о том, что произошло изменение имени, однако вы можете увидеть в моем включенном коде, что после того, как вы установили имя в значение 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;
            }
        } 
    }
}