Правильное использование ThreadContext в Log4Net для асинхронных операций
Узнайте, как правильно использовать ThreadContext в Log4Net для асинхронных операций с async/await. Узнайте, почему LogicalThreadContext необходим для предотвращения сохранения свойств между несколькими асинхронными задачами и реализации правильной изоляции контекста.
Как правильно использовать ThreadContext в Log4Net для избежания сохранения свойств между несколькими асинхронными операциями?
Я реализую обработку сообщений с Log4Net в многопоточной среде. Вот моя текущая реализация:
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:
log4net.ThreadContext.Properties["id"] = message.MessageID;
Этот ID используется в моей конфигурации AdoNetAppender:
<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
- Решение: LogicalThreadContext
- Правильные шаблоны управления контекстом
- Лучшие практики реализации
- Альтернативные подходы
- Обработка очистки контекста
- Тестирование и валидация
Понимание проблемы с ThreadContext
Основная причина вашей проблемы заключается в том, как ThreadContext.Properties работает в асинхронных средах. Когда вы устанавливаете свойство с помощью ThreadContext.Properties["id"] = message.MessageID, вы сохраняете его в локальном хранилище потока (TLS). Однако при использовании await выполнение может возобновиться на любом доступном потоке из пула потоков, возможно, на другом, чем тот, где контекст был изначально установлен.
Как объясняется в обсуждении на Stack Overflow, это создает проблему, при которой:
// вызывающий поток (поток #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.
Вот как следует изменить вашу реализацию:
// Вместо:
log4net.ThreadContext.Properties["id"] = message.MessageID;
// Используйте:
log4net.LogicalThreadContext.Properties["id"] = message.MessageID;
Ключевое отличие заключается в том, что LogicalThreadContext полагается не на локальное хранилище потока, а на логический контекст вызова, который распространяется вместе с контекстом выполнения через асинхронные операции. Как объясняет Wiktor Zychla:
Именно это делает логический контекст потока, он реализован с использованием CallContext, и свойства правильно сохраняются в коде async/await.
Правильные шаблоны управления контекстом
Для вашего сценария обработки сообщений следует реализовать правильное управление контекстом для обеспечения изоляции между различными операциями обработки сообщений. Вот рекомендуемые шаблоны:
1. Использование using для автоматической очистки
private async Task ProcessMessageAsync(Message message)
{
using (LogicalThreadContext.Properties["id"].Set(message.MessageID))
{
// Ваша логика обработки сообщений здесь
await ProcessMessageContent(message);
// Все логирование в этом контексте будет иметь правильный ID
_logger.WriteLog(LogLevel.Info, "Сообщение обработано");
}
// Вне using свойство автоматически удаляется
}
2. Метод расширения для более чистого синтаксиса
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);
}
}
Затем используйте его так:
private async Task ProcessMessageAsync(Message message)
{
using (LogicalThreadContext.Properties.Set("id", message.MessageID))
{
// Ваша логика обработки сообщений
}
}
Лучшие практики реализации
1. Всегда используйте LogicalThreadContext для асинхронного кода
// Для сценариев async/await предпочтительнее LogicalThreadContext
LogicalThreadContext.Properties["correlationId"] = correlationId;
LogicalThreadContext.Properties["tenantId"] = tenantId;
// ThreadContext надежно работает только для синхронного кода
ThreadContext.Properties["requestId"] = requestId; // Не рекомендуется для async
2. Настройте ваш аппендер для логического контекста
Убедитесь, что ваш AdoNetAppender настроен на использование свойств логического контекста:
<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. Обрабатывайте распространение контекста в сложных сценариях
Для сложных сценариев с вложенными асинхронными вызовами убедитесь, что контекст правильно распространяется:
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>:
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:
private async Task ProcessMessageAsync(Message message)
{
using (LogicalThreadContext.Stacks["NDC"].Push(message.MessageID))
{
await ProcessMessageContent(message);
}
}
// Конфигурация макета
<conversionPattern value="%ndc" />
3. Использование отображаемого диагностического контекста (MDC)
Для пар “ключ-значение” можно использовать MDC с LogicalThreadContext:
LogicalThreadContext.Properties["messageId"] = message.MessageID;
LogicalThreadContext.Properties["timestamp"] = DateTime.UtcNow.ToString();
Обработка очистки контекста
Правильная очистка критически важна для предотвращения утечки контекста между операциями. Вот несколько подходов:
1. Использование шаблона IDisposable
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 с автоматическим сбросом
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. Юнит-тесты для изоляции контекста
[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. Интеграционные тесты для асинхронных сценариев
[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. Проверка логирования
Настройте тестовые аппендеры для захвата вывода логов и проверки свойств контекста:
[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 для асинхронных операций:
- Всегда используйте LogicalThreadContext вместо ThreadContext для сценариев async/await
- Реализуйте правильную очистку контекста с использованием шаблонов IDisposable или
usingоператоров - Настройте ваши аппендеры для использования свойств логического контекста
- Тщательно тестируйте для обеспечения изоляции контекста между параллельными операциями
- Рассмотрите альтернативы такие как AsyncLocal или NDC для сложных сценариев
Следуя этим шаблонам, вы обеспечите правильную изоляцию свойств логирования между различными асинхронными операциями, предотвращая возникновение проблем с сохранением контекста, которые вы испытываете.
Источники
- В чем разница между log4net.ThreadLogicalContext и log4net.ThreadContext - Stack Overflow
- Объектно-ориентированная разработка программного обеспечения: Common.Logging над log4Net и сохранение специфичных для события свойств в async коде
- Поддержка LogicalThreadContext в log4net · Issue #80 · damianh/LibLog
- LogicalThreadContext в log4net работает не так, как ожидается - Stack Overflow
- Cup(Of T): проблемы с контекстом log4net с thread agility ASP.Net
- c# - как управлять стеком log4net NDC-подобным образом с помощью async/await методов? - Stack Overflow
- Класс ThreadContext - Apache log4net