CallContext.LogicalGetData восстанавливается даже там, где нет асинхронности. Зачем? - программирование

CallContext.LogicalGetData восстанавливается даже там, где нет асинхронности. Зачем?

Я заметил, что CallContext.LogicalSetData/LogicalGetData не работает так, как я ожидал от них. Значение, установленное внутри метода async, восстанавливается , даже если нет асинхронности или любого типа переключения потоков.

Вот простой пример:

using System;
using System.Runtime.Remoting.Messaging;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication
{
    class Program
    {
        static async Task<int> TestAsync()
        {
            CallContext.LogicalSetData("valueX", "dataX");
            // commented out on purpose
            // await Task.FromResult(0); 
            Console.WriteLine(CallContext.LogicalGetData("valueX"));
            return 42;
        }

        static void Main(string[] args)
        {
            using(ExecutionContext.SuppressFlow())
            {
                CallContext.LogicalSetData("valueX", "dataXX");
                Console.WriteLine(CallContext.LogicalGetData("valueX"));
                Console.WriteLine(TestAsync().Result);
                Console.WriteLine(CallContext.LogicalGetData("valueX"));
            }
        }
    }
}

Он производит этот вывод:

dataXX
dataX
42
dataXX

Если я делаю TestAsync не-async, он работает как ожидалось:

static Task<int> TestAsync()
{
    CallContext.LogicalSetData("valueX", "dataX");
    Console.WriteLine(CallContext.LogicalGetData("valueX"));
    return Task.FromResult(42);
}

Вывод:

dataXX
dataX
42
dataX

Я бы понял это поведение, если бы у меня была какая-то настоящая асинхронность внутри TestAsync, но это не так. Я даже использую ExecutionContext.SuppressFlow, но это ничего не меняет.

Может кто-нибудь объяснить, почему он работает таким образом?

4b9b3361

Ответ 1

"Как и ожидалось" в этом случае разные для разных людей.:)

В исходном Async CTP (который не модифицировал какой-либо код фреймворка) не было никакой поддержки контекста типа "асинхронный". MS добавила эту переменную в значение LocalCallContext в .NET 4.5. Старое поведение (с общим логическим контекстом) особенно проблематично при работе с асинхронным concurrency (т.е. Task.WhenAll).

Я объясняю высокоуровневую механику LocalCallContext внутри методов async в своем блоге. Ключ здесь:

Когда запускается метод async, он уведомляет свой контекст логического вызова, чтобы активировать поведение копирования на запись.

В контексте логического вызова есть специальный флаг копирования на запись, который включается всякий раз, когда начинает запускаться метод async. Это выполняется конечным автоматом async (в частности, в текущей реализации AsyncMethodBuilderCore.Start вызывает ExecutionContext.EstablishCopyOnWriteScope). И "флаг" - это упрощение - нет никакого фактического логического элемента или чего-то еще; он просто изменяет состояние (ExecutionContextBelongsToCurrentScope и друзей) таким образом, что любая будущая запись будет (мелкой) скопировать контекст логического вызова.

Тот же метод конечных автоматов (Start) будет вызывать ExecutionContextSwitcher.Undo всякий раз, когда это делается с синхронной частью метода async. Это то, что восстанавливает прежний логический контекст.