.net 核心 AsyncLocal 失去价值

Hoo*_*Yao 3 c# async-await

我使用与HttpContextAccessor类似的模式

简化版如下,Console.WriteLine(SimpleStringHolder.StringValue)不应该是null。

public class SimpleStringHolder
{
    private static readonly AsyncLocal<ValueHolder> CurrentHolder = new AsyncLocal<ValueHolder>();

    public static string StringValue
    {
        get => CurrentHolder.Value?.StringValue;

        set
        {
            var holder = CurrentHolder.Value;
            if (holder != null)
            {
                holder.StringValue = null;
            }

            if (value != null)
            {
                CurrentHolder.Value = new ValueHolder() { StringValue = value };
            }
        }
    }

    private class ValueHolder
    {
        public string StringValue;
    }
}
Run Code Online (Sandbox Code Playgroud)
class Program
{
    private static readonly AsyncLocal<string> currentValue = new AsyncLocal<string>();

    public static void Main(string[] args)
    {
        var task = Task.Run(async () => await OutterAsync());
        task.Wait();
    }

    public static async Task OutterAsync()
    {
        SimpleStringHolder.StringValue = "1";
        await InnerAsync();
        Console.WriteLine(SimpleStringHolder.StringValue); //##### the value is gone ######
    }

    public static async Task InnerAsync()
    {
        var lastValue = SimpleStringHolder.StringValue;
        await Task.Delay(1).ConfigureAwait(false);
        SimpleStringHolder.StringValue = lastValue; // comment this line will make it work
        Console.WriteLine(SimpleStringHolder.StringValue); //the value is still here
    }
}

Run Code Online (Sandbox Code Playgroud)

在上述代码中,OutterAsync调用异步方法InnerAsync,在InnerAsync所述StringValue 被设置,这使得AsyncLocal失去其上下文中OutterAsync Console.WriteLine(SimpleStringHolder.StringValue);为空。

我认为神奇之处在于 SimpleStringHolder 的属性集,删除以下代码将使事情正确。

if (holder != null)
{
    holder.StringValue = null;
}

Run Code Online (Sandbox Code Playgroud)

上面的代码按预期工作。

请帮我理解这是什么魔法?

Pet*_*iho 5

AsyncLocal<T>存在是为了提供一种在异步执行上下文中保存值的机制。对此的关键是您的示例中涉及的两个因素:

  1. Anawait允许一个方法返回给调用者,这可能会改变上下文。对于旧ThreadLocal<T>类型,当执行将控制权返回给方法时,它可能在不同的线程中,即使从async上下文的角度来看也是相同的。UsingAsyncLocal<T>确保await在可等待对象完成后将控制权返回给方法时恢复上下文的状态。
  2. 在需要改变上下文的事情发生之前,AsyncLocal<T>对象的当前状态是它以前的状态。即方法本质上继承了对象在被调用时所处的状态。如果您正在处理简单的值,则不会有任何意外,但是对于像您的ValueHolder类型这样的引用类型,唯一AsyncLocal<T>要跟踪的是对该对象的引用。仍然只有对象的一个​​副本,并且任何给定的此类对象的状态更改都像它们总是在有或没有浮动的异步上下文(即它们可以通过对该对象的任何引用看到)的情况下工作。

因此,在您提供的代码示例中:

  1. OutterAsync()StringValue属性设置为"1",这会导致ValueHolder创建一个新对象,并将该StringValue对象的属性设置为"1"
  2. OutterAsync()调用InnerAsync()。然后该方法string从持有者那里检索引用(间接……即通过SimpleStringHolder.StringValue属性)。由于此时没有对值和上下文进行任何更改ValueHolder,因此在这种情况下使用了相同的对象,因此您可以"1"返回。
  3. InnerAsync()等待一个异步任务,这会导致创建一个新的执行上下文,以便将AsyncValue<T>对该上下文的对象所做的更改隔离开来。从这一点开始,不同上下文中的代码将不到对对象的更改。例如,在OutterAsync()方法中执行的代码。
  4. 异步任务在 中完成后InnerAsync(),该方法然后为该SimpleStringHolder.StringValue属性设置一个新值。因为较早的上下文是继承的,所以当 setter 设置holder.StringValue为 时null,它正在设置在 中创建的对象的属性OutterAsync()但是……因为代码在一个新的上下文中,当 setter 为CurrentHolder.Value属性分配一个新值时,该更改与该上下文隔离。
  5. 当该InnerAsync()方法最终完成时,这完成了该OutterAsync()方法await正在等待的任务。这会导致AsyncValue<T>将其状态恢复到OutterAsync()方法的上下文,这与InnerAsync()它更新SimpleStringHolder.StringValue值时所处的上下文不同。具体地说,这个恢复的状态是对ValueHolder最初在属性设置为 nullSimpleStringHolder时设置的对象的引用holder.StringValue
  6. 因此,当然OutterAsync()后查看属性值时,它发现它设置为 null。因为它被设置为空。

在您自己的实验中,您可以完全删除空赋值,也可以简单地SimpleStringHolder.StringValueInnerAsync()'sawait语句之后省略赋值(因为如果不进行赋值,则永远不会执行空赋值)。无论哪种方式,都不会发生空分配,因此之前分配的值仍然存在。

但是,如果您确实进行了空分配,则调用者OutterAsync()将恢复其上下文,然后恢复持有者对象引用,并且该持有者对象自己的string引用已经设置为null,所以这就是OutterAsync()所见。

相关阅读:
AsyncLocal 在非 async/await 代码中的作用是什么?
为什么代码稍微重构一下,AsyncLocal 会返回不同的结果?
难道AsyncLocal也做那些事ThreadLocal吗?