как управлять стеком log4net, подобным NDC, с помощью методов async/await? (для стека задач?)

В обычном/синхронном/однопоточном консольном приложении NDC.Push отлично работает для управления «текущим элементом» (возможно, на нескольких уровнях вложенности, но в этом примере только 1 уровень).

Например:

private static ILog s_logger = LogManager.GetLogger("Program");

static void Main(string[] args)
{
    BasicConfigurator.Configure();

    DoSomeWork("chunk 1");
    DoSomeWork("chunk 2");
    DoSomeWork("chunk 3");
}

static void DoSomeWork(string chunkName)
{
    using (NDC.Push(chunkName))
    {
        s_logger.Info("Starting to do work");
        Thread.Sleep(5000);
        s_logger.Info("Finishing work");
    }
}

Это приведет к выходу журнала ожиданий, показывающему запись NDC «chunk X» справа от «Program» (шаблон по умолчанию для базового конфигуратора).

232 [9] INFO Часть программы 1 - Приступаем к работе

5279 [9] ИНФО Часть программы 1 - отделочные работы

5279 [9] INFO Программный фрагмент 2 - Начало работы

10292 [9] ИНФО Часть программы 2 - отделочные работы

10292 [9] INFO Программный фрагмент 3 - Начало работы

15299 [9] ИНФО Часть программы 3 - Отделочные работы

Однако я не могу понять, как поддерживать это, используя «обычные» асинхронные методы.

Например, пытаясь сделать это:

private static ILog s_logger = LogManager.GetLogger("Program");

static void Main(string[] args)
{
    BasicConfigurator.Configure();

    var task1 = DoSomeWork("chunk 1");
    var task2 = DoSomeWork("chunk 2");
    var task3 = DoSomeWork("chunk 3");

    Task.WaitAll(task1, task2, task3);
}

static async Task DoSomeWork(string chunkName)
{
    using (log4net.LogicalThreadContext.Stacks["NDC"].Push(chunkName))
    //using (log4net.ThreadContext.Stacks["NDC"].Push(chunkName))
    {
        s_logger.Info("Starting to do work");
        await Task.Delay(5000);
        s_logger.Info("Finishing work");
    }
}

Показывает, что все они запускаются нормально, но когда задача завершается в другом потоке, стек теряется (я надеялся, что log4net.LogicalThreadContext будет «осведомленным» о TPL).

234 [10] INFO Часть программы 1 - Приступаем к работе

265 [10] INFO Часть программы 2 - Приступаем к работе

265 [10] INFO Часть программы 3 - Приступаем к работе

5280 [7] Программа INFO (нулевая) - отделочные работы

5280 [12] Программа INFO (нулевая) - отделочные работы

5280 [12] Программа INFO (нулевая) - отделочные работы

Помимо добавления нового TaskContext (или подобного) в log4net, есть ли способ отслеживания такого рода активности?

На самом деле цель состоит в том, чтобы сделать это с помощью синтаксического сахара async/await - либо форсировать какое-то сходство с потоком, либо делать такие вещи, как сохранение параллельного словаря вокруг ключевого слова по задаче, вероятно, являются работоспособными вариантами, но я пытаюсь держаться как можно ближе к синхронная версия кода, насколько это возможно. :)


person James Manning    schedule 20.03.2012    source источник
comment
К вашему сведению, недавно я обнаружил, что Microsoft исправила CallContext в .NET 4.5 RTW для работы с async. Таким образом, NDC log4net и другие решения, использующие Logical*Data, будут работать, как и ожидалось, с методами async (только в .NET 4.5).   -  person Stephen Cleary    schedule 08.03.2013
comment
@StephenCleary круто! Спасибо!   -  person James Manning    schedule 22.03.2013
comment
Я не уверен, что проблема связана с контекстом логического потока. Реализация log4net кажется мне неправильной, потому что родительский и дочерний потоки используют один и тот же стек. Дочерний элемент должен получить клон родительского стека, чтобы, если родитель изменяет стек, это не мешало дочернему...   -  person dou bret    schedule 26.11.2014


Ответы (1)


Сейчас нет хорошей истории для async контекстов логических вызовов.

CallContext нельзя использовать для этого. Логический CallContext не понимает, как методы async возвращаются раньше и возобновляют работу позже, поэтому он не всегда будет правильно работать с кодом, использующим простой параллелизм, таким как Task.WhenAll.

Обновление: CallContext был обновлен в .NET 4.5 RTW для корректной работы с async методами.

Я посмотрел в log4net; LogicalThreadContext задокументировано как использование CallContext, но была ошибка, из-за которой он использовал нелогические контексты (исправлено в их SVN 2 февраля 2012 г.; текущий выпуск 1.2.11 не включает это исправление). Однако, даже когда это будет исправлено, оно все равно не будет работать с async (поскольку логическое CallContext не работает с async).

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

А пока проголосуйте за предложение, чтобы Microsoft предоставила какой-то механизм для этого.

P.S. Параллельный словарь с ключом Task не будет работать, потому что методы async не обязательно являются запущенными задачами (т. е. в вашем примере кода в операторе using Task.CurrentId будет null, потому что в этот момент фактически не выполняется задача).

И сходство потоков тоже имеет свои проблемы. На самом деле вам понадобится отдельный поток для каждой независимой асинхронной операции. Прощай, масштабируемость...

person Stephen Cleary    schedule 20.03.2012
comment
Извините, я не ясно выразился - для подхода с параллельным словарем я бы вообще не использовал асинхронные методы, а вместо этого «явно» использовал бы задачи (так, что вы уже можете делать в VS2010/.NET4). Хотел бы я просто прикрепить данные к задаче (ну, без создания подклассов и создания собственных) и получить «текущий контекст задачи», аналогичный HttpContext.Current, хотя я тоже не думал об этом. :) - person James Manning; 20.03.2012
comment
Я понимаю. Что ж, если вы хотите прикрепить данные к Task (кроме AsyncState), вы можете использовать Подключенные свойства, но IMO так же легко использовать ConcurrentDictionary для этого сценария. - person Stephen Cleary; 20.03.2012
comment
@StephenCleary Стивен Туб ответил на ваш связанный пост в User Voice, заявив, что ваша исходная информация устарела, а CallContext подходит для async/await. - person Lex; 28.08.2012
comment
@Lex: Обновлено. Обратите внимание, что CallContext — несмотря на то, что его можно использовать, — все равно не решит проблему. - person Stephen Cleary; 06.01.2013
comment
Похоже, что это окончательно исправлено в log4net 1.2.14 - см. stackoverflow.com/a/28868941/2144 - person Eric Pohl; 26.04.2016
comment
@Eric Pohl, у меня 1.2.15, и LogicalThreadContext по-прежнему не сохраняется с async/await. - person Sergey Akopov; 19.11.2016
comment
Я использую log4net 2.08, и свойства LogicalThreadContext действительно переносятся между разными потоками, когда используется ключевое слово async. - person jk1990; 23.08.2018