ExecutionContext не передает стек вызовов из асинхронных методов - PullRequest
4 голосов
/ 02 апреля 2019

Рассмотрим следующий код:

private static async Task Main(string[] args)
{
    await SetValueInAsyncMethod();
    PrintValue();

    await SetValueInNonAsyncMethod();
    PrintValue();
}

private static readonly AsyncLocal<int> asyncLocal = new AsyncLocal<int>();

private static void PrintValue([CallerMemberName] string callingMemberName = "")
{
    Console.WriteLine($"{callingMemberName}: {asyncLocal.Value}");
}

private static async Task SetValueInAsyncMethod()
{
    asyncLocal.Value = 1;
    PrintValue();

    await Task.CompletedTask;
}

private static Task SetValueInNonAsyncMethod()
{
    asyncLocal.Value = 2;
    PrintValue();

    return Task.CompletedTask;
}

Если вы запустите этот код в консольном приложении .NET 4.7.2, вы получите следующий вывод:

SetValueInAsyncMethod: 1
Main: 0
SetValueInNonAsyncMethod: 2
Main: 2

Я понимаю, что различия в выводе проистекают из того факта, что SetValueInAsyncMethod на самом деле не метод, а конечный автомат, выполняемый AsyncTaskMethodBuilder, который захватывает ExecutionContext внутри, а SetValueInNonAsyncMethod - просто обычный метод ,

Но даже с учетом этого понимания у меня все еще есть несколько вопросов:

  1. Это ошибка / отсутствующая функция или намеренное дизайнерское решение?
  2. Нужно ли беспокоиться об этом поведении при написании кода, который зависит от AsyncLocal? Скажем, я хочу написать свой TransactionScope -продолжительный объект, который передает некоторые окружающие данные, хотя и ожидает точки AsyncLocal достаточно здесь?
  3. Существуют ли другие альтернативы AsyncLocal и CallContext.LogicalGetData / CallContext.LogicalSetData в .NET, когда дело доходит до сохранения значений в «потоке логического кода»?

Ответы [ 2 ]

5 голосов
/ 02 апреля 2019

Мне кажется, это намеренное решение.

Как вы уже знаете, SetValueInAsyncMethod компилируется в конечный автомат, который неявно захватывает текущий ExecutionContext. Когда вы изменяете переменную AsyncLocal, это изменение не возвращается обратно к вызывающей функции. Напротив, SetValueInNonAsyncMethod не является асинхронным и, следовательно, не компилируется в конечный автомат. Следовательно, ExecutionContext не фиксируется, и любые изменения в AsyncLocal -переменных видны для вызывающей стороны.

Вы также можете захватить ExecutionContext самостоятельно, если вам это нужно по какой-либо причине:

private static Task SetValueInNonAsyncMethodWithEC()
{
    var ec = ExecutionContext.Capture(); // Capture current context into ec
    ExecutionContext.Run(ec, _ => // Use ec to run the lambda
    {
        asyncLocal.Value = 3;
        PrintValue();
    });
    return Task.CompletedTask;
}

Это выдаст значение 3, а Main выдаст 2.

Конечно, проще преобразовать SetValueInNonAsyncMethod в асинхронный, чтобы компилятор сделал это за вас.

Что касается кода, который использует AsyncLocal (или CallContext.LogicalGetData в этом отношении), важно знать, что изменение значения в вызываемом асинхронном методе (или любом захваченном ExecutionContext) не будет «возвращаться». Но вы, конечно, можете по-прежнему обращаться к AsyncLocal и изменять его, если вы не переназначаете его.

4 голосов
/ 02 апреля 2019

Это ошибка / отсутствующая функция или намеренное дизайнерское решение?

Это намеренное дизайнерское решение. В частности, конечный автомат async устанавливает флаг «копировать при записи» для своего логического контекста.

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

Нужно ли беспокоиться об этом поведении при написании кода, который зависит от AsyncLocal? Скажем, я хочу написать свой TransactionScope-wannabe, который передает некоторые окружающие данные, хотя и ожидает точки. Достаточно ли здесь AsyncLocal?

Большинство подобных систем используют AsyncLocal<T> в сочетании с шаблоном IDisposable, который очищает значение AsyncLocal<T>. Объединение этих шаблонов гарантирует, что он будет работать с синхронным или асинхронным кодом. AsyncLocal<T> будет нормально работать, если код потребления является методом async; использование его с IDisposable гарантирует, что он будет работать как с async, так и с синхронными методами.

Существуют ли другие альтернативы AsyncLocal и CallContext.LogicalGetData / CallContext.LogicalSetData в .NET, когда дело доходит до сохранения значений в «потоке логического кода»?

номер

...