AsyncLocal的运作机制和陷阱

这是今天帮柠檬分析一个AsyncLocal相关的问题时发现的.
试想这个代码输出的值是多少?

using System;
using System.Threading;
using System.Threading.Tasks;

namespace asynclocal
{
    class Program
    {
        public static AsyncLocal<int> v = new AsyncLocal<int>();

        static void Main(string[] args)
        {
            var task = Task.Run(async () =>
            {
                v.Value = 123;
                var intercept = new Intercept();
                await Intercept.Invoke();
                Console.WriteLine(Program.v.Value);
            });
            task.Wait();
        }
    }

    public class Intercept
    {
        public static async Task Invoke()
        {
            Program.v.Value = 888;
        }
    }
}

答案是123.
为什么修改了AsyncLocal的值却无效呢?

这要从AsyncLocal的运作机制说起.
首先这是AsyncLocal的源代码:

public T Value
{
    get
    {
        object obj = ExecutionContext.GetLocalValue(this);
        return (obj == null) ? default(T) : (T)obj;
    }
    set
    {
        ExecutionContext.SetLocalValue(this, value, m_valueChangedHandler != null);
    }
}

获取和设置值用的是ExecutionContext.GetLocalValueExecutionContext.SetLocalValue这两个静态函数.
这两个静态函数的源代码在ExecutionContext中:

internal static object GetLocalValue(IAsyncLocal local)
{
    ExecutionContext current = Thread.CurrentThread.ExecutionContext;
    if (current == null)
        return null;

    object value;
    current.m_localValues.TryGetValue(local, out value);
    return value;
}

internal static void SetLocalValue(IAsyncLocal local, object newValue, bool needChangeNotifications)
{
    ExecutionContext current = Thread.CurrentThread.ExecutionContext ?? ExecutionContext.Default;

    object previousValue;
    bool hadPreviousValue = current.m_localValues.TryGetValue(local, out previousValue);

    if (previousValue == newValue)
        return;

    IAsyncLocalValueMap newValues = current.m_localValues.Set(local, newValue);

    //
    // Either copy the change notification array, or create a new one, depending on whether we need to add a new item.
    //
    IAsyncLocal[] newChangeNotifications = current.m_localChangeNotifications;
    if (needChangeNotifications)
    {
        if (hadPreviousValue)
        {
            Debug.Assert(Array.IndexOf(newChangeNotifications, local) >= 0);
        }
        else
        {
            int newNotificationIndex = newChangeNotifications.Length;
            Array.Resize(ref newChangeNotifications, newNotificationIndex + 1);
            newChangeNotifications[newNotificationIndex] = local;
        }
    }

    Thread.CurrentThread.ExecutionContext =
        new ExecutionContext(newValues, newChangeNotifications, current.m_isFlowSuppressed);

    if (needChangeNotifications)
    {
        local.OnValueChanged(previousValue, newValue, false);
    }
}

看到SetLocalValue里面的处理了吗? 每一次修改值以后都会生成一个新的执行上下文然后覆盖到当前的线程对象上.

我们再来看看调用一个异步函数时的代码:

// Token: 0x06000004 RID: 4 RVA: 0x000020B0 File Offset: 0x000002B0
.method public hidebysig static 
    class [System.Runtime]System.Threading.Tasks.Task Invoke () cil managed 
{
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [System.Runtime]System.Type) = (
        01 00 21 61 73 79 6e 63 6c 6f 63 61 6c 2e 49 6e
        74 65 72 63 65 70 74 2b 3c 49 6e 76 6f 6b 65 3e
        64 5f 5f 30 00 00
    )
    .custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = (
        01 00 00 00
    )
    // Header Size: 12 bytes
    // Code Size: 52 (0x34) bytes
    // LocalVarSig Token: 0x11000002 RID: 2
    .maxstack 2
    .locals init (
        [0] class asynclocal.Intercept/'<Invoke>d__0',
        [1] valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder
    )

    /* 0x000002BC 7309000006   */ IL_0000: newobj    instance void asynclocal.Intercept/'<Invoke>d__0'::.ctor()
    /* 0x000002C1 0A           */ IL_0005: stloc.0
    /* 0x000002C2 06           */ IL_0006: ldloc.0
    /* 0x000002C3 281700000A   */ IL_0007: call      valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Create()
    /* 0x000002C8 7D05000004   */ IL_000C: stfld     valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder asynclocal.Intercept/'<Invoke>d__0'::'<>t__builder'
    /* 0x000002CD 06           */ IL_0011: ldloc.0
    /* 0x000002CE 15           */ IL_0012: ldc.i4.m1
    /* 0x000002CF 7D04000004   */ IL_0013: stfld     int32 asynclocal.Intercept/'<Invoke>d__0'::'<>1__state'
    /* 0x000002D4 06           */ IL_0018: ldloc.0
    /* 0x000002D5 7B05000004   */ IL_0019: ldfld     valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder asynclocal.Intercept/'<Invoke>d__0'::'<>t__builder'
    /* 0x000002DA 0B           */ IL_001E: stloc.1
    /* 0x000002DB 1201         */ IL_001F: ldloca.s  1
    /* 0x000002DD 1200         */ IL_0021: ldloca.s  0
    /* 0x000002DF 280100002B   */ IL_0023: call      instance void [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start<class asynclocal.Intercept/'<Invoke>d__0'>(!!0&)
    /* 0x000002E4 06           */ IL_0028: ldloc.0
    /* 0x000002E5 7C05000004   */ IL_0029: ldflda    valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder asynclocal.Intercept/'<Invoke>d__0'::'<>t__builder'
    /* 0x000002EA 281900000A   */ IL_002E: call      instance class [System.Runtime]System.Threading.Tasks.Task [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::get_Task()
    /* 0x000002EF 2A           */ IL_0033: ret
} // end of method Intercept::Invoke

异步函数会编译成一个状态机(类型)然后通过System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start执行,
System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start的源代码如下:

/// <summary>Initiates the builder's execution with the associated state machine.</summary>
/// <typeparam name="TStateMachine">Specifies the type of the state machine.</typeparam>
/// <param name="stateMachine">The state machine instance, passed by reference.</param>
[DebuggerStepThrough]
public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
    if (stateMachine == null) // TStateMachines are generally non-nullable value types, so this check will be elided
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine);
    }

    // Run the MoveNext method within a copy-on-write ExecutionContext scope.
    // This allows us to undo any ExecutionContext changes made in MoveNext,
    // so that they won't "leak" out of the first await.

    Thread currentThread = Thread.CurrentThread;
    ExecutionContextSwitcher ecs = default(ExecutionContextSwitcher);
    try
    {
        ExecutionContext.EstablishCopyOnWriteScope(currentThread, ref ecs);
        stateMachine.MoveNext();
    }
    finally
    {
        ecs.Undo(currentThread);
    }
}

执行状态机前会调用ExecutionContext.EstablishCopyOnWriteScope, 源代码如下:

internal static void EstablishCopyOnWriteScope(Thread currentThread, ref ExecutionContextSwitcher ecsw)
{
    Debug.Assert(currentThread == Thread.CurrentThread);

    ecsw.m_ec = currentThread.ExecutionContext;
    ecsw.m_sc = currentThread.SynchronizationContext;
}

执行状态机后会调用ExecutionContextSwitcher::Undo, 源代码如下:

internal void Undo(Thread currentThread)
{
    Debug.Assert(currentThread == Thread.CurrentThread);

    // The common case is that these have not changed, so avoid the cost of a write if not needed.
    if (currentThread.SynchronizationContext != m_sc)
    {
        currentThread.SynchronizationContext = m_sc;
    }

    if (currentThread.ExecutionContext != m_ec)
    {
        ExecutionContext.Restore(currentThread, m_ec);
    }
}

总结起来:

  • AsyncLocal每设置一次值就会创建一个新的ExecutionContext并覆盖到Thread.CurrentThread.ExecutionContext
  • 执行状态机前会备份当前的Thread.CurrentThread.ExecutionContext
  • 执行状态机后会恢复备份的Thread.CurrentThread.ExecutionContext

再来看看文章开头我给出的代码中的处理流程:

  • 初始的执行上下文为空, 且叫 { }
  • 修改AsyncLocal的值到123后, 执行上下文变为 { <int>: 123 }
  • 调用Intercept.Invoke前备份了执行上下文, 备份的是 { <int>: 123 }
  • Intercept.Invoke修改AsyncLocal的值到888后, 执行上下文变为 { <int>: 888 }
  • 调用Intercept.Invoke后恢复备份的上下文, 恢复后是 { <int>: 123 }

到这里就很清楚了.
await外的AsyncLocal值可以传递到await内, await内的AsyncLocal值无法传递到await外(只能读取不能修改).
这个问题在StackOverflow上有人提过, 但回应很少.

微软是故意这样设计的, 否则就无法实现MSDN上的这个例子了.
但我个人认为这是个设计错误, 柠檬她给出的例子本意是想在aop拦截器中覆盖AsyncLocal中的Http上下文, 但明显这样做是行不通的.
我建议编写csharp代码时尽可能的不要使用ThreadLocal和AsyncLocal.

posted @ 2017-10-28 15:55  q303248153  阅读(3019)  评论(7编辑  收藏  举报