CallContext.LogicalGetData 即使在没有异步的情况下也会恢复。为什么?

CallContext.LogicalGetData gets restored even where there is no asynchrony. Why?

我注意到 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 非异步,它会按预期工作:

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

输出:

dataXX
dataX
42
dataX

如果我在 TestAsync 中有一些真正的异步,我会理解这种行为,但这里不是这种情况。我什至使用 ExecutionContext.SuppressFlow,但这并没有改变任何东西。

有人可以解释一下为什么会这样吗?

"As expected" 在这种情况下对于不同的人来说是不同的。 :)

在最初的 Async CTP(没有修改任何框架代码)中,根本不支持 "async-local" 种上下文。 MS 修改了 .NET 4.5 中的 LocalCallContext 以添加此支持。旧行为(具有共享逻辑上下文)是 especially problematic when working with asynchronous concurrency (i.e., Task.WhenAll).

我在我的博客上解释 high-level mechanics of LocalCallContext within async methods。关键在这里:

When an async method starts, it notifies its logical call context to activate copy-on-write behavior.

只要 async 方法开始执行,逻辑调用上下文中就有一个特殊的写时复制标志。这是由 async 状态机完成的(具体来说,在当前实现中,AsyncMethodBuilderCore.Start 调用 ExecutionContext.EstablishCopyOnWriteScope)。 "flag" 是一种简化——没有实际的布尔成员或任何东西;它只是修改状态(ExecutionContextBelongsToCurrentScope 和朋友),任何未来的写入都会(浅)复制逻辑调用上下文。

相同的状态机方法 (Start) 将在完成 async 方法的同步部分时调用 ExecutionContextSwitcher.Undo。这就是恢复以前的逻辑上下文。