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
, но это ничего не меняет.
Может кто-нибудь объяснить, почему он работает таким образом?
Ответы
Ответ 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
. Это то, что восстанавливает прежний логический контекст.