代码之家  ›  专栏  ›  技术社区  ›  Aloraman

如何检查在相同的“异步上下文”中访问AsyncLocal

  •  4
  • Aloraman  · 技术社区  · 7 年前

    TL;博士 ThreadLocal<T>.Value 指向相同位置,如果 Thread.CurrentThread 保持不变。有类似的吗 AsyncLocal<T>.Value (例如会 SychronizationContext.Current ExecutionContext.Capture() 是否满足所有场景)?


    假设我们创建了一些数据结构的快照,这些快照保存在线程本地存储中(例如。 ThreadLocal<T> 实例),并将其传递给A腋窝类以供以后使用。这个腋窝类用于将此数据结构恢复到快照状态。我们不想将此快照恢复到不同的线程上,所以我们可以检查创建了哪个线程A腋窝类。例如:

    class Storage<T>
    {
        private ThreadLocal<ImmutableStack<T>> stackHolder;
    
        public IDisposable Push(T item)
        {
            var bookmark = new StorageBookmark<T>(this);
            stackHolder.Value = stackHolder.Value.Push(item);
            return bookmark;
        }
    
        private class StorageBookmark<TInner> :IDisposable
        {
            private Storage<TInner> owner;
            private ImmutableStack<TInner> snapshot;
            private Thread boundThread;
    
            public StorageBookmark(Storage<TInner> owner)
            { 
                 this.owner = owner;
                 this.snapshot = owner.stackHolder.Value;
                 this.boundThread  = Thread.CurrentThread;
            }
    
            public void Dispose()
            {
                 if(Thread.CurrentThread != boundThread) 
                     throw new InvalidOperationException ("Bookmark crossed thread boundary");
                 owner.stackHolder.Value = snapshot;
            }
        }
    }
    

    通过这种方式,我们基本上将StorageBookmark绑定到特定的线程,从而绑定到ThreadLocal存储中数据结构的特定版本。我们做到了这一点,确保在 线当前线程

    现在,来回答眼前的问题。我们如何实现相同的行为 AsyncLocal<T> 而不是 螺纹局部(<T> ? 准确地说,有什么类似于 线当前线程 可以在构造和使用时检查,以控制“异步上下文”未被跨越(这意味着 异步本地(<T>。价值 将指向与构建书签时相同的对象)。
    似乎是 SynchronizationContext.Current ExecutionContext。捕获() 可能就足够了,但我不确定哪一个更好,也不确定是否有陷阱(甚至在所有可能的情况下都会有用)

    2 回复  |  直到 7 年前
        1
  •  2
  •   TheXenocide    4 年前

    您希望做的是从根本上违背异步执行上下文的本质;您不需要(因此也不能保证)立即等待在异步上下文中创建的所有任务,等待的顺序与创建任务的顺序相同,或者根本不需要等待,但在调用上下文的范围内创建任务会使它们成为同一异步上下文周期的一部分。

    将异步执行上下文视为不同于线程上下文可能很有挑战性,但异步性并不是并行性的同义词,而并行性正是逻辑线程所支持的。存储在线程本地存储中、不打算跨线程共享/复制的对象通常是可变的,因为逻辑线程内的执行始终能够保证相对受限的顺序逻辑(如果为了确保编译时优化不会给您带来麻烦,可能需要进行一些特殊处理,尽管这很少见,而且只在非常特定的场景中才有必要)。因此 ThreadLocal 在您的示例中,不需要 ImmutableStack ,可能只是 Stack (它的性能要好得多),因为您不需要担心写时拷贝或并发访问。如果堆栈是可公开访问的,那么更重要的是有人可以将堆栈传递给其他线程,这些线程可以推送/弹出项目,但由于这是一个私有的实现细节 ImmutableStack 实际上可能被视为不必要的复杂性。

    无论如何,执行上下文,这不是一个独特的概念。NET(在其他平台上的实现可能在某些方面有所不同,尽管在我的经验中从来没有太多)非常类似于调用堆栈(并与之直接相关),但在某种程度上,它将新的异步任务视为堆栈上的新调用,这可能需要两者共享调用方在执行操作时的状态,以及分歧,因为调用方可能会继续创建更多任务,并以在读取连续指令集时不具有逻辑意义的方式创建/更新状态。通常建议在 ExecutionContext 是不可变的,尽管在某些情况下,仍然指向同一实例引用的上下文的所有副本都必须共享可变数据。 IHttpContext 例如,存储在 IHttpContextAccessor 使用 AsyncLocal ,因此,例如,在单个请求范围内创建的所有任务都可以访问相同的响应状态。允许多个并发上下文对同一引用实例进行突变必然会从并发性和逻辑执行顺序两方面引入问题的可能性。例如,多个任务试图在HTTP响应上设置不同的结果将导致异常或意外行为。在某种程度上,您可以尝试在这里帮助消费者,但归根结底,消费者有责任了解他们所依赖的细微实现细节的复杂性(这通常是一种代码味道,但在现实世界中有时是一种必要的邪恶)。

    除此之外,如前所述,为了确保所有嵌套上下文都能以可预测和安全的方式运行,通常建议只存储不可变的类型,并始终将上下文恢复到其以前的值(就像使用一次性堆栈机制所做的那样)。考虑写时复制行为最简单的方法是,好像每个新任务、新线程池工作项和新线程都有自己的上下文克隆,但如果它们指向相同的引用类型(即所有线程都有相同的 引用指针 )它们都有相同的实例;写时拷贝只是一种优化,可以在不必要时防止拷贝,但本质上可以完全忽略,并认为每个逻辑任务都有自己的拷贝 上下文的 (非常像这样 ImmutableStack 或字符串)。如果更新当前值的唯一方法 不可变集合项指向的 就是将其重新分配给一个新的修改过的实例,然后您就不必担心跨上下文污染(就像那样 ImmutableStack 您正在使用)。

    您的示例没有显示任何关于如何访问数据或传入数据的类型的内容 T 因此,无法看出您可能会面临什么问题,但如果您所关心的是处理“当前”上下文的嵌套任务,或者将IDisposable值分配给某个字段并从不同的线程访问,那么您可以尝试以下几件事,并且有几点值得考虑:

    • 与您当前支票最接近的等价物是:
    if(stackHolder.Value != this) 
        throw new InvalidOperationException ("Bookmark disposed out of order or in wrong context");
    
    • 一个简单的 ObjectDisposedException 如果两个上下文尝试处理异常,则将至少从一个上下文引发异常。
    • 虽然通常不建议这样做,但如果您想绝对确定对象至少被释放一次,可以在 IDisposable 实施(确保调用 GC.SuppressFinalize(this) 在Dispose方法中)。
    • 通过将前两者结合起来,虽然不能保证它是在创建它的某个任务/方法块中被释放的,但至少可以保证对象被释放一次且仅被释放一次。
    • 由于方法的根本重要性 ExecutionContext 应该是流动和控制的,执行引擎(通常是运行时、任务调度器等,但在第三方以新颖的方式使用任务/线程的情况下)有责任确保 ExecutionContext 在适当的情况下,捕获并抑制流量。如果在根上下文中发生线程、调度程序或同步迁移,则 ExecutionContext 不应流入下一个逻辑任务的 ExecutionContext 线程/调度程序在以前执行任务的上下文中进行处理。例如,如果任务继续从 ThreadPool 线程,然后等待继续,这将导致下一个逻辑操作在不同的 线程池 当原始线程返回 线程池 它不应继续引用/流动 ExecutionContext 不再在其中逻辑执行的任务。假设没有额外的任务被并行创建并误入歧途,一旦在根等待器中恢复执行,它将是唯一继续引用上下文的执行上下文。当任务完成时,它的执行上下文(或者更确切地说,它的副本)也会完成。
    • 即使未观察到的后台任务已启动且从未等待,如果存储在 异步本地 是不可变的,写时拷贝行为与不可变堆栈相结合将确保执行上下文的并行克隆永远不会相互污染
    • 使用第一次检查并与不可变类型一起使用时,您真的不需要担心克隆的并行执行上下文,除非您担心它们从以前的上下文中获取敏感数据;当它们处理当前项时,只有当前执行上下文(特别是嵌套的并行上下文)的堆栈恢复到前一个;不会修改所有克隆的上下文(包括父上下文)。
    • 如果你 担心嵌套上下文通过处理不应该访问的内容来访问父数据有相对简单的模式可以用来分隔 IDisposable可识别 来自环境值,以及中使用的抑制模式 TransactionScope 例如,将当前值临时设置为 null

    以实际的方式重申,例如,您存储 ImmutableList 在您的一个书签中。如果项目存储在 不可变列表 是可变的,则上下文污染是可能的。

    
    var someImmutableListOfMutableItems = unsafeAsyncLocal.Value;
    // sets Name for all contexts pointing to the reference type.
    someImmutableListOfMutableItems[0].Name = "Jon"; // assigns property setter on shared reference of Person
    // notice how unsafeAsyncLocal.Value never had to be reassigned?
    

    然而,不可变项的不可变集合永远不会污染另一个上下文,除非在执行上下文的流动方式方面存在根本性的错误(联系供应商、提交错误报告、发出警报等)

    var someImmutableListOfImmutableItems = safeAsyncLocal.Value;
    
    someImmutableListOfImmutableItems = someImmutableListOfImmutableItems.SetItem(0, 
        someImmutableListOfImmutableItems[0].SetName("Jon") // SetName returns a new immutable Person instance
    ); // SetItem returns new immutable list instance
    
    // notice both the item and the collection need to be reassigned. No other context will be polluted here
    safeAsyncLocal.Value = someImmutableListOfImmutableItems;
    

    编辑:一些文章是写给那些想读一些比我在这里漫谈更连贯的东西的人的:)

    https://devblogs.microsoft.com/pfxteam/executioncontext-vs-synchronizationcontext/ https://weblogs.asp.net/dixin/understanding-c-sharp-async-await-3-runtime-context

    为了进行一些比较,这里有一篇关于如何在JavaScript中管理上下文的文章,JavaScript是单线程的,但支持异步编程模型(我想这可能有助于说明它们之间的联系/区别):

    https://blog.bitsrc.io/understanding-execution-context-and-execution-stack-in-javascript-1c9ea8642dd0

        2
  •  1
  •   Kevin Gosse    7 年前

    逻辑调用上下文与执行上下文具有相同的流语义,因此 AsyncLocal . 知道了这一点,您可以在逻辑上下文中存储一个值,以便在跨越“异步上下文”边界时进行检测:

    class Storage<T>
    {
        private AsyncLocal<ImmutableStack<T>> stackHolder = new AsyncLocal<ImmutableStack<T>>();
    
        public IDisposable Push(T item)
        {
            var bookmark = new StorageBookmark<T>(this);
    
            stackHolder.Value = (stackHolder.Value ?? ImmutableStack<T>.Empty).Push(item);
            return bookmark;
        }
    
        private class StorageBookmark<TInner> : IDisposable
        {
            private Storage<TInner> owner;
            private ImmutableStack<TInner> snapshot;
            private Thread boundThread;
            private readonly object id;
    
            public StorageBookmark(Storage<TInner> owner)
            {
                id = new object();
                this.owner = owner;
                this.snapshot = owner.stackHolder.Value;
                CallContext.LogicalSetData("AsyncStorage", id);
            }
    
            public void Dispose()
            {
                if (CallContext.LogicalGetData("AsyncStorage") != id)
                    throw new InvalidOperationException("Bookmark crossed async context boundary");
                owner.stackHolder.Value = snapshot;
            }
        }
    }
    
    public class Program
    {
        static void Main()
        {
            DoesNotThrow().Wait();
            Throws().Wait();
        }
    
        static async Task DoesNotThrow()
        {
            var storage = new Storage<string>();
    
            using (storage.Push("hello"))
            {
                await Task.Yield();
            }
        }
    
        static async Task Throws()
        {
            var storage = new Storage<string>();
    
            var disposable = storage.Push("hello");
    
            using (ExecutionContext.SuppressFlow())
            {
                Task.Run(() => { disposable.Dispose(); }).Wait();
            }
        }
    }