如何检查在同一&#34;异步上下文中访问AsyncLocal <t>&#34;

时间:2018-01-12 10:30:50

标签: c# .net async-await task-parallel-library

如果ThreadLocal<T>.Value保持不变,则

TL; DR Thread.CurrentThread指向同一位置。 AsyncLocal<T>.Value是否有类似的内容(例如SychronizationContext.CurrentExecutionContext.Capture()是否足以满足所有情况?)

想象一下,我们已经创建了一些数据结构的快照,该快照保存在线程本地存储(例如ThreadLocal<T>实例)中,并将其传递给axillary类供以后使用。此axilic类用于将此数据结构还原为快照状态。我们不想将此快照恢复到不同的线程,因此我们可以检查创建了哪个线程腋窝类。例如:

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存储中的特定版本的数据结构。我们这样做是为了确保我们不会跨越线索背景&#34;在Thread.CurrentThread的帮助下 现在,提出问题。我们如何使用AsyncLocal<T>代替ThreadLocal<T>来实现相同的行为?确切地说,是否有类似于Thread.CurrentThread的任何内容,可以在构造和使用时检查以控制&#34;异步上下文&#34;没有被交叉(这意味着AsyncLocal<T>.Value将指向与构建书签时相同的对象。) 似乎SynchronizationContext.CurrentExecutionContext.Capture()可能就足够了,但我不确定哪个更好,没有捕获(甚至可以在所有可能的情况下都有效)

2 个答案:

答案 0 :(得分:2)

逻辑调用上下文与执行上下文具有相同的流语义,因此为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();
        }
    }
}

答案 1 :(得分:1)

您希望做的事情与异步执行上下文的本质完全相反。您不需要(因此不能保证)将立即等待异步环境中创建的所有任务,无论它们是按照创建它们的顺序,还是完全不会等待,而是在调用上下文的范围内创建它们使它们成为同一异步上下文周期的一部分。

将异步执行上下文与线程上下文不同可能是具有挑战性的,但是异步并不是并行性的同义词,并行性是逻辑线程特别支持的。线程本地存储中不打算在线程之间共享/复制的对象通常可以是可变的,因为在逻辑线程内执行将始终能够保证相对受约束的顺序逻辑(如果可能需要采取某些特殊处理来确保编译-时间优化不会惹您,尽管这种情况很少见,只有在非常特定的情况下才有必要。因此,您示例中的ThreadLocal并不一定要是ImmutableStack,它可能只是一个Stack(具有更好的性能),因为您不需要担心写时复制或并发访问。如果堆栈是可公开访问的,那么将有人将堆栈传递给其他可以推送/弹出项目的线程会更加令人担忧,但是由于这里是一个私有实现细节,ImmutableStack实际上可以看作是不必要的复杂性。

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

如前所述,为了确保所有嵌套上下文的运行可预测且安全,通常不建议使用该方案,通常建议仅存储不可变的类型,并始终将上下文还原为其以前的值(就像处理一次性堆栈一样)机制)。考虑写时复制行为的最简单方法是,每个新任务,新线程池工作项和新线程都会获得自己的上下文克隆,但是如果它们指向相同的引用类型(即所有具有相同引用指针的副本),它们都具有相同的实例;写时复制只是一种优化,可以防止在不必要时进行复制,但实际上可以完全忽略,并且可以认为这是每个逻辑任务都具有自己的上下文副本 (非常类似于ImmutableStack或字符串)。如果更新不可变集合项所指向的当前值的唯一方法是将其重新分配给新的修改实例,那么您就不必担心跨上下文污染(就像{ {1}}您正在使用)。

您的示例未显示有关ImmutableStack的数据访问方式或传递的类型的任何信息,因此无法查看您可能遇到的问题,但是如果您担心的是嵌套的布置“当前”上下文的任务或将IDisposable值分配给某个位置的字段并从其他线程访问的任务,您可以尝试一些操作,并且需要考虑以下几点:

  • 与您当前支票最接近的等效项是:
T
  • 如果两个上下文试图处理一个简单的if(stackHolder.Value != this) throw new InvalidOperationException ("Bookmark disposed out of order or in wrong context"); ,则至少会抛出一个异常。
  • 尽管通常不建议这样做,但是如果您要确定对象至少已处置一次,则可以在ObjectDisposedException实现的终结器中抛出异常(请务必调用{{1} })。
  • 通过合并前两个,虽然不能保证将其放置在创建它的确切任务/方法块中,但您至少可以保证一个对象只能放置一次。
  • 由于应该IDisposable的方式进行流动和控制,因此这是执行引擎(通常是运行时,任务计划程序等)的责任,但在第三方的情况下正在以新颖的方式使用任务/线程),以确保GC.SuppressFinalize(this)流在适当的情况下被捕获和抑制。如果在根上下文中发生线程,调度程序或同步迁移,则不应在先前执行任务的上下文中将ExecutionContext流到下一个逻辑任务的ExecutionContext线程/调度程序进程中。例如,如果任务继续在ExecutionContext线程上开始,然后等待继续,导致下一个逻辑操作在与原来开始的线程或其他I / O资源不同的ExecutionContext线程上继续进行完成线程,当返回原始线程ThreadPool时,它不应继续引用/流向不再在其中逻辑执行的任务的ThreadPool。假设没有并行创建其他任务并且将其误入歧途,则一旦在根等待者中恢复执行,它将是唯一继续引用该上下文的执行上下文。任务完成后,其执行上下文(或它的副本)也将完成。
  • 即使启动并从未等待未观察到的后台任务,如果存储在ThreadPool中的数据是不可变的,写时复制行为与不可变的堆栈结合将确保执行上下文的并行克隆永远不会互相污染
  • 通过第一次检查并与不可变类型一起使用,您真的不需要担心克隆的并行执行上下文,除非您担心它们会从以前的上下文中获取敏感数据的访问权限;当他们处置当前项目时,只有当前执行上下文(例如,嵌套的并行上下文)的堆栈恢复为上一个;所有克隆的上下文(包括父上下文)都不会被修改。
  • 如果您 担心嵌套上下文通过处理它们不应该访问的东西来访问父数据,则可以使用相对简单的模式将ExecutionContext与环境值分开例如AsyncLocal中使用的抑制模式,例如暂时将当前值设置为IDisposable,等等。

仅以实际的方式进行重申,例如,假设您将TransactionScope存储在一个书签中。如果存储在null中的项目是可变的,则可能造成上下文污染。

ImmutableList

不可变项的不可变集合将永远不会污染另一个上下文,除非从根本上说执行上下文如何流动(联系供应商,提交错误报告,发出警报等)是错误的

ImmutableList

编辑:一些文章供那些想阅读比我这里的杂谈更连贯的人:)

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

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

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