Как управлять NDC-подобным стеком log4net с помощью методов async/wait? (для каждой задачи?)

В обычном/синхронном/однопоточном консольном приложении 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");
    }
}

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

232 [9] INFO Программный фрагмент 1 - Запуск работы

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

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

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

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

15299 [9] INFO Программный фрагмент 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 Program (null) - Отделочные работы

5280 [12] INFO Program (null) - Отделочные работы

5280 [12] INFO Program (null) - Отделочные работы

Вне добавления нового файла TaskContext (или подобного) в log4net существует ли способ отслеживать этот вид активности?

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

Ответ 1

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

CallContext не может быть использован для этого. Логический CallContext не понимает, как методы async возвращаются рано и возобновляются позже, поэтому он не всегда будет корректно работать для кода, который использует простой parallelism, например 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), потому что нет задача, выполняемая в этой точке).

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