Другое

Правильное использование ThreadContext в Log4Net для асинхронных операций

Узнайте, как правильно использовать ThreadContext в Log4Net для асинхронных операций с async/await. Узнайте, почему LogicalThreadContext необходим для предотвращения сохранения свойств между несколькими асинхронными задачами и реализации правильной изоляции контекста.

Как правильно использовать ThreadContext в Log4Net для избежания сохранения свойств между несколькими асинхронными операциями?

Я реализую обработку сообщений с Log4Net в многопоточной среде. Вот моя текущая реализация:

csharp
var messageProcesses = (from message in messageQueue.Messages 
                    select ProcessMessageAsync(message)).ToList();
try
{
    await Task.WhenAll(messageProcesses).NoState();
}
catch (Exception e)
{
    ...
}
_logger.WriteLog(LogLevel.Info, "Processing Done.");

Внутри ProcessMessageAsync(message) я устанавливаю свойство ThreadContext:

csharp
log4net.ThreadContext.Properties["id"] = message.MessageID;

Этот ID используется в моей конфигурации AdoNetAppender:

xml
<commandText value="INSERT INTO mds.MessageSenderLog ([Id]) VALUES (@id)" />
...
<parameter>
  <parameterName value="@id" />
  <dbType value="String" />
  <size value="50" />
  <layout type="log4net.Layout.PatternLayout">
    <conversionPattern value="%property{id}" />
  </layout>
</parameter>

Проблема в том, что при логировании “Processing Done” включается предыдущее значение @id, как будто это глобальное значение, а не значение, ограниченное конкретной обработкой сообщения.

Какой правильный способ использования ThreadContext в этой ситуации для обеспечения правильной изоляции свойств между различными асинхронными операциями?

Содержание

Понимание проблемы с ThreadContext

Основная причина вашей проблемы заключается в том, как ThreadContext.Properties работает в асинхронных средах. Когда вы устанавливаете свойство с помощью ThreadContext.Properties["id"] = message.MessageID, вы сохраняете его в локальном хранилище потока (TLS). Однако при использовании await выполнение может возобновиться на любом доступном потоке из пула потоков, возможно, на другом, чем тот, где контекст был изначально установлен.

Как объясняется в обсуждении на Stack Overflow, это создает проблему, при которой:

csharp
// вызывающий поток (поток #1)
log4net.ThreadContext.Properties["MyProp"] = "123";

// теперь установлено в потоке #1
log("start");

await Task.WhenAll(
    MyAsync(1), // Проблема, если задача выполняется в потоке #1, она будет иметь "MyProp"
    MyAsync(2)  // Проблема, если задача выполняется в потоке #1, она будет иметь "MyProp"
);

log("end"); // Проблема только по случайности вы снова выполнитесь в потоке #1

Это объясняет, почему ваше сообщение “Processing Done” показывает ID предыдущего сообщения - контекст из другой асинхронной операции повторно используется при окончательном логировании.


Решение: LogicalThreadContext

Правильный подход для сценариев async/await - использовать LogicalThreadContext.Properties вместо ThreadContext.Properties. Как отмечено в исследованиях, LogicalThreadContext использует класс .NET CallContext, который правильно распространяется через границы async/await.

Вот как следует изменить вашу реализацию:

csharp
// Вместо:
log4net.ThreadContext.Properties["id"] = message.MessageID;

// Используйте:
log4net.LogicalThreadContext.Properties["id"] = message.MessageID;

Ключевое отличие заключается в том, что LogicalThreadContext полагается не на локальное хранилище потока, а на логический контекст вызова, который распространяется вместе с контекстом выполнения через асинхронные операции. Как объясняет Wiktor Zychla:

Именно это делает логический контекст потока, он реализован с использованием CallContext, и свойства правильно сохраняются в коде async/await.

Правильные шаблоны управления контекстом

Для вашего сценария обработки сообщений следует реализовать правильное управление контекстом для обеспечения изоляции между различными операциями обработки сообщений. Вот рекомендуемые шаблоны:

1. Использование using для автоматической очистки

csharp
private async Task ProcessMessageAsync(Message message)
{
    using (LogicalThreadContext.Properties["id"].Set(message.MessageID))
    {
        // Ваша логика обработки сообщений здесь
        await ProcessMessageContent(message);
        
        // Все логирование в этом контексте будет иметь правильный ID
        _logger.WriteLog(LogLevel.Info, "Сообщение обработано");
    }
    
    // Вне using свойство автоматически удаляется
}

2. Метод расширения для более чистого синтаксиса

csharp
public static class LogicalThreadContextExtensions
{
    public static IDisposable SetProperty(string key, object value)
    {
        LogicalThreadContext.Properties[key] = value;
        return new PropertyCleanup(key);
    }
}

public class PropertyCleanup : IDisposable
{
    private readonly string _key;
    
    public PropertyCleanup(string key)
    {
        _key = key;
    }
    
    public void Dispose()
    {
        LogicalThreadContext.Properties.Remove(_key);
    }
}

Затем используйте его так:

csharp
private async Task ProcessMessageAsync(Message message)
{
    using (LogicalThreadContext.Properties.Set("id", message.MessageID))
    {
        // Ваша логика обработки сообщений
    }
}

Лучшие практики реализации

1. Всегда используйте LogicalThreadContext для асинхронного кода

csharp
// Для сценариев async/await предпочтительнее LogicalThreadContext
LogicalThreadContext.Properties["correlationId"] = correlationId;
LogicalThreadContext.Properties["tenantId"] = tenantId;

// ThreadContext надежно работает только для синхронного кода
ThreadContext.Properties["requestId"] = requestId; // Не рекомендуется для async

2. Настройте ваш аппендер для логического контекста

Убедитесь, что ваш AdoNetAppender настроен на использование свойств логического контекста:

xml
<appender name="AdoNetAppender" type="log4net.Appender.AdoNetAppender">
    <bufferSize value="1" />
    <connectionType value="System.Data.SqlClient.SqlConnection, System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
    <connectionString value="ваша_строка_подключения" />
    <commandText value="INSERT INTO mds.MessageSenderLog ([Id]) VALUES (@id)" />
    
    <parameter>
        <parameterName value="@id" />
        <dbType value="String" />
        <size value="50" />
        <layout type="log4net.Layout.PatternLayout">
            <conversionPattern value="%property{id}" />
        </layout>
    </parameter>
</appender>

3. Обрабатывайте распространение контекста в сложных сценариях

Для сложных сценариев с вложенными асинхронными вызовами убедитесь, что контекст правильно распространяется:

csharp
public async Task ProcessBatchAsync(IEnumerable<Message> messages)
{
    var tasks = messages.Select(async message =>
    {
        // Установите контекст для этого конкретного сообщения
        using (LogicalThreadContext.Properties.Set("id", message.MessageID))
        {
            // Этот контекст будет сохраняться во вложенных асинхронных вызовах
            await ProcessMessageWithSubOperations(message);
        }
    });
    
    await Task.WhenAll(tasks);
}

Альтернативные подходы

1. Использование AsyncLocal для пользовательского контекста

Если вам нужен больший контроль над управлением контекстом, вы можете использовать AsyncLocal<T>:

csharp
private static readonly AsyncLocal<string> MessageIdContext = new AsyncLocal<string>();

public async Task ProcessMessageAsync(Message message)
{
    MessageIdContext.Value = message.MessageID;
    
    try
    {
        await ProcessMessageContent(message);
    }
    finally
    {
        MessageIdContext.Value = null;
    }
}

// В вашем макете используйте пользовательский макет, который считывает из AsyncLocal

2. Использование вложенного диагностического контекста (NDC)

Для иерархических контекстов логирования рассмотрите использование NDC:

csharp
private async Task ProcessMessageAsync(Message message)
{
    using (LogicalThreadContext.Stacks["NDC"].Push(message.MessageID))
    {
        await ProcessMessageContent(message);
    }
}

// Конфигурация макета
<conversionPattern value="%ndc" />

3. Использование отображаемого диагностического контекста (MDC)

Для пар “ключ-значение” можно использовать MDC с LogicalThreadContext:

csharp
LogicalThreadContext.Properties["messageId"] = message.MessageID;
LogicalThreadContext.Properties["timestamp"] = DateTime.UtcNow.ToString();

Обработка очистки контекста

Правильная очистка критически важна для предотвращения утечки контекста между операциями. Вот несколько подходов:

1. Использование шаблона IDisposable

csharp
public class MessageContextScope : IDisposable
{
    private readonly string _key;
    private readonly object _previousValue;
    
    public MessageContextScope(string key, object value)
    {
        _key = key;
        _previousValue = LogicalThreadContext.Properties[key];
        LogicalThreadContext.Properties[key] = value;
    }
    
    public void Dispose()
    {
        if (_previousValue != null)
        {
            LogicalThreadContext.Properties[_key] = _previousValue;
        }
        else
        {
            LogicalThreadContext.Properties.Remove(_key);
        }
    }
}

// Использование
using (new MessageContextScope("id", message.MessageID))
{
    // Логика обработки
}

2. Использование AsyncLocal с автоматическим сбросом

csharp
private static readonly AsyncLocal<MessageContext> _currentMessage = new AsyncLocal<MessageContext>();

public async Task ProcessMessageAsync(Message message)
{
    var previousContext = _currentMessage.Value;
    _currentMessage.Value = new MessageContext { MessageID = message.MessageID };
    
    try
    {
        await ProcessMessageContent(message);
    }
    finally
    {
        _currentMessage.Value = previousContext;
    }
}

public class MessageContext
{
    public string MessageID { get; set; }
}

Тестирование и валидация

Чтобы убедиться, что ваше управление контекстом работает правильно, реализуйте следующие стратегии тестирования:

1. Юнит-тесты для изоляции контекста

csharp
[Test]
public async Task ProcessMessageAsync_DолженИзолироватьКонтекстМеждуСообщениями()
{
    // Arrange
    var message1 = new Message { MessageID = "msg1" };
    var message2 = new Message { MessageID = "msg2" };
    
    // Act
    var task1 = ProcessMessageAsync(message1);
    var task2 = ProcessMessageAsync(message2);
    
    await Task.WhenAll(task1, task2);
    
    // Assert
    // Убедитесь, что контекст был правильно изолирован
    Assert.IsNull(LogicalThreadContext.Properties["id"]);
}

private async Task ProcessMessageAsync(Message message)
{
    using (LogicalThreadContext.Properties.Set("id", message.MessageID))
    {
        await Task.Delay(10); // Симуляция асинхронной работы
        // Проверка контекста во время обработки
        Assert.AreEqual(message.MessageID, LogicalThreadContext.Properties["id"]);
    }
    // Проверка очистки контекста
    Assert.IsNull(LogicalThreadContext.Properties["id"]);
}

2. Интеграционные тесты для асинхронных сценариев

csharp
[Test]
public async Task ProcessMultipleMessagesAsync_DолжноСохранятьПравильныйКонтекст()
{
    // Arrange
    var messages = new List<Message>
    {
        new Message { MessageID = "1" },
        new Message { MessageID = "2" },
        new Message { MessageID = "3" }
    };
    
    // Act
    await ProcessBatchAsync(messages);
    
    // Assert
    // Финальный контекст должен быть чистым
    Assert.IsNull(LogicalThreadContext.Properties["id"]);
}

3. Проверка логирования

Настройте тестовые аппендеры для захвата вывода логов и проверки свойств контекста:

csharp
[Test]
public async Task ProcessMessageAsync_DолженЛогироватьСПравильнымIDСообщения()
{
    // Arrange
    var message = new Message { MessageID = "test123" };
    var testAppender = new MemoryAppender();
    LogManager.GetRepository().Root.AddAppender(testAppender);
    
    // Act
    await ProcessMessageAsync(message);
    
    // Assert
    var events = testAppender.GetEvents();
    Assert.IsTrue(events.Any(e => e.RenderedMessage.Contains("test123")));
}

Заключение

Чтобы правильно использовать ThreadContext в Log4Net для асинхронных операций:

  1. Всегда используйте LogicalThreadContext вместо ThreadContext для сценариев async/await
  2. Реализуйте правильную очистку контекста с использованием шаблонов IDisposable или using операторов
  3. Настройте ваши аппендеры для использования свойств логического контекста
  4. Тщательно тестируйте для обеспечения изоляции контекста между параллельными операциями
  5. Рассмотрите альтернативы такие как AsyncLocal или NDC для сложных сценариев

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

Источники

  1. В чем разница между log4net.ThreadLogicalContext и log4net.ThreadContext - Stack Overflow
  2. Объектно-ориентированная разработка программного обеспечения: Common.Logging над log4Net и сохранение специфичных для события свойств в async коде
  3. Поддержка LogicalThreadContext в log4net · Issue #80 · damianh/LibLog
  4. LogicalThreadContext в log4net работает не так, как ожидается - Stack Overflow
  5. Cup(Of T): проблемы с контекстом log4net с thread agility ASP.Net
  6. c# - как управлять стеком log4net NDC-подобным образом с помощью async/await методов? - Stack Overflow
  7. Класс ThreadContext - Apache log4net
Авторы
Проверено модерацией
Модерация