Trace.CorrelationManager.LogicalOperationStack
позволяет иметь вложенные идентификаторы логической операции, где наиболее распространенным случаем является регистрация (NDC). Должна ли она работать с async-await
?
Вот простой пример с использованием LogicalFlow
, который является моей простой оболочкой над LogicalOperationStack
:
private static void Main() => OuterOperationAsync().GetAwaiter().GetResult();
private static async Task OuterOperationAsync()
{
Console.WriteLine(LogicalFlow.CurrentOperationId);
using (LogicalFlow.StartScope())
{
Console.WriteLine("\t" + LogicalFlow.CurrentOperationId);
await InnerOperationAsync();
Console.WriteLine("\t" + LogicalFlow.CurrentOperationId);
await InnerOperationAsync();
Console.WriteLine("\t" + LogicalFlow.CurrentOperationId);
}
Console.WriteLine(LogicalFlow.CurrentOperationId);
}
private static async Task InnerOperationAsync()
{
using (LogicalFlow.StartScope())
{
await Task.Delay(100);
}
}
LogicalFlow
:
public static class LogicalFlow
{
public static Guid CurrentOperationId =>
Trace.CorrelationManager.LogicalOperationStack.Count > 0
? (Guid) Trace.CorrelationManager.LogicalOperationStack.Peek()
: Guid.Empty;
public static IDisposable StartScope()
{
Trace.CorrelationManager.StartLogicalOperation();
return new Stopper();
}
private static void StopScope() =>
Trace.CorrelationManager.StopLogicalOperation();
private class Stopper : IDisposable
{
private bool _isDisposed;
public void Dispose()
{
if (!_isDisposed)
{
StopScope();
_isDisposed = true;
}
}
}
}
Вывод:
00000000-0000-0000-0000-000000000000
49985135-1e39-404c-834a-9f12026d9b65
54674452-e1c5-4b1b-91ed-6bd6ea725b98
c6ec00fd-bff8-4bde-bf70-e073b6714ae5
54674452-e1c5-4b1b-91ed-6bd6ea725b98
Конкретные значения на самом деле не имеют значения, но, как я понимаю, обе внешние линии должны показывать Guid.Empty
(т.е. 00000000-0000-0000-0000-000000000000
), а внутренние строки должны показывать то же значение Guid
.
Вы могли бы сказать, что LogicalOperationStack
использует Stack
, который не является потокобезопасным и почему результат неправильный. Но в то время как это верно в общем случае, в этом случае не более одного потока, обращающегося к LogicalOperationStack
в то же время (каждая операция async
ожидается при вызове и без использования комбинаторов, таких как Task.WhenAll
)
Проблема заключается в том, что LogicalOperationStack
хранится в CallContext
, который имеет поведение копирования на запись. Это означает, что до тех пор, пока вы явно не установите что-то в CallContext
(а вы не добавляете в существующий стек с StartLogicalOperation
), вы используете родительский контекст, а не свой собственный.
Это можно показать, просто установив что-либо в CallContext
перед добавлением в существующий стек. Например, если мы изменили StartScope
на это:
public static IDisposable StartScope()
{
CallContext.LogicalSetData("Bar", "Arnon");
Trace.CorrelationManager.StartLogicalOperation();
return new Stopper();
}
Вывод:
00000000-0000-0000-0000-000000000000
fdc22318-53ef-4ae5-83ff-6c3e3864e37a
fdc22318-53ef-4ae5-83ff-6c3e3864e37a
fdc22318-53ef-4ae5-83ff-6c3e3864e37a
00000000-0000-0000-0000-000000000000
Примечание. Я не предлагаю, чтобы кто-то на самом деле это делал. Реальное практическое решение состояло бы в том, чтобы использовать ImmutableStack
вместо LogicalOperationStack
, поскольку он и поточно-безопасный, и поскольку он неизменен, когда вы вызываете Pop
, вы возвращаете новый ImmutableStack
, который затем вам нужно вернуть обратно CallContext
. В качестве ответа на этот вопрос доступна полная реализация: Отслеживание потока С#/.NET задач
Итак, должен ли LogicalOperationStack
работать с async
и это просто ошибка? Является ли LogicalOperationStack
просто не для мира async
? Или я что-то упускаю?
Обновление. Использование Task.Delay
, по-видимому, запутывается, поскольку использует System.Threading.Timer
, который захватывает ExecutionContext
внутренне. Использование await Task.Yield();
вместо await Task.Delay(100);
упрощает понимание примера.