DI容器托管实例中的线程同步

时间:2017-08-14 13:24:27

标签: c# asp.net multithreading asp.net-web-api autofac

我有一个"build:aot:prod": "node --max_old_space_size=8092 ./node_modules/@angular/cli/bin/ng build --prod --aot" 类,它从ASP.NET Web API应用程序中的请求中捕获一些特定于HTTP请求的运行时值

FooContext

多个消费者依赖于此public class FooContext { private readonly ISet<string> _set = new HashSet<string>(); public void AddToSet(string s) => _set.Add(s); // Copied so that caller won't modify _set public ISet<string> GetStrings() => new HashSet<string>(_set); } ,并会调用FooContext / AddToSet,并根据结果运行不同的业务逻辑。

我想保证每个HTTP请求每个GetStrings只有一个实例,所以我在DI容器中注册为请求范围(在这里使用Autofac作为示例,但我猜大多数容器大致相同):

FooContext

我的理解是,protected override void Load(ContainerBuilder builder) { builder.RegisterType<FooContext>().InstancePerRequest(); } 不是线程安全的,因为线程可能会在同一个FooContext实例上同时调用GetStrings / AddToSet(因为它是请求-scoped)。 无法保证每个HTTP请求都能在一个线程上完成

我没有明确地创建新线程,也没有在我的应用程序中调用FooContext,但我确实使用了很多Task.Run()async-await,这意味着延续可能在不同的线程上。

我的问题是:

  1. ConfigureAwait(false)是不是线程安全的?我的理解是否正确?
  2. 如果这确实是线程不安全,并且我想允许多个读者但只允许一个独家作者,我应该在FooContext上应用ReaderWriterLockSlim吗?
  3. 更新

    由于评论者评论说我的问题无法显示而没有显示ISet<string>的用法,我会在这里做。我在FooContext中使用FooContext来捕获在控制器方法中传递的几个参数:

    IAutofacActionFilter

    然后在其他控制业务逻辑的服务类中:

    public class FooActionFilter : IAutofacActionFilter
    {
        private readonly FooContext _fooContext;
    
        public FooActionFilter(FooContext fooContext)
            => _fooContext = fooContext;
    
        public Task OnActionExecutingAsync(
            HttpActionContext actionContext, 
            CancellationToken cancellationToken)
        {
            var argument = (string)actionContext.ActionArguments["mystring"];
            _fooContext.AddToSet(argument);
            return Task.CompletedTask;
        }
    }
    

2 个答案:

答案 0 :(得分:3)

  

无法保证每个HTTP请求都能在一个线程上完成。

当使用async / await时,请求可能在多个线程上运行,但请求将流动从一个线程到另一个线程,这意味着该请求将不会在中的多个线程上运行平行

这意味着基于每个请求缓存的类不必是线程安全的,因为它们的状态不是并行访问的。它们确实需要能够顺序从多个线程进行访问,但如果您开始存储线程ID或任何其他线程关联状态(例如,使用ThreadLocal<T>),这通常只是一个问题。 p>

因此,不要进行任何特殊同步或使用任何并发数据结构(例如ConcurrentDictionary),因为它只会使代码复杂化,而不需要这样做(除非您忘记await对于某些操作,因为在这种情况下你会意外地并行运行操作,这会导致各种各样的问题。欢迎来到async / await的美丽世界。

  

多个使用者依赖于此FooContext并将调用AddToSet / GetStrings,并根据结果运行不同的业务逻辑。

这些消费者可以依赖FooContext,只要他们的有效期为TransientInstancePerRequest。这对他们的消费者来说一直是调用图。如果违反此规则,您将拥有Captive Dependency,这可能会导致您的FooContext实例被多个请求重用,这将导致并发问题。

在多线程应用程序中使用DI时,您必须要小心。 This documentation确实提供了一些指示。

答案 1 :(得分:2)

  

我没有明确地创建新线程,也没有在我的应用程序中调用Task.Run(),但我确实使用了很多async-await和ConfigureAwait(false),这意味着延续可能在不同的线程上。

是的,所以它是以多线程方式访问的。

  

FooContext不是线程安全的吗?我的理解是否正确?

我会说调用代码实际上是错误的:ConfigureAwait(false)表示&#34;我不需要请求上下文&#34;但是如果代码使用FooContext(请求上下文的一部分,然后确实需要请求上下文。

  

如果这确实是线程不安全,并且我想允许多个读者但只允许一个独占作者,我应该在ISet上应用ReaderWriterLockSlim吗?

几乎肯定不是。 lock可能会正常工作。

使用读取器/写入器锁只是因为某些代码执行读操作而其他代码执行写操作是一个常见错误。只有当某些代码执行读取,其他代码执行写入以及存在大量并发读取访问权限时,才需要读取器/写入器锁定。