【问题标题】:Thread synchronization in DI container managed instancesDI 容器托管实例中的线程同步
【发布时间】:2017-08-14 13:24:27
【问题描述】:

我有一个 FooContext 类,它从 ASP.NET Web API 应用程序内的请求中捕获一些特定于 HTTP 请求的运行时值

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/GetStrings,根据结果,运行不同的业务逻辑。

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

protected override void Load(ContainerBuilder builder)
{
    builder.RegisterType<FooContext>().InstancePerRequest();
}

我的理解是,FooContext 不是线程安全的,因为线程可能在同一个 FooContext 实例上同时调用 GetStrings/AddToSet(因为它是请求范围的)。 不保证每个 HTTP 请求都会在一个线程上完成

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

我的问题是:

  1. FooContext 真的不是线程安全的吗?我上面的理解正确吗?
  2. 如果这确实是线程不安全的,并且我想允许多个读者但只有一个独占作者,我应该在ISet&lt;string&gt; 上应用ReaderWriterLockSlim 吗?

更新

由于评论者认为我的问题在没有显示FooContext 的用法的情况下无法回答,所以我会在这里进行。我在IAutofacActionFilter 中使用FooContext 来捕获在控制器方法中传递的几个参数:

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;
    }
}

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

public class BarService
{
    private readonly FooContext _fooContext;

    public BarService(FooContext fooContext)
        => _fooContext = fooContext;

    public async Task DoSomething()
    {
         var strings = _fooContext.GetStrings();
         if (strings.Contains("foo"))
         {
             // Do something
         }
    }
}

【问题讨论】:

  • 1. Yes. 在实践中,只要您一次只针对给定的 HTTP 请求运行一个线程(换句话说 - 它问题不是在请求的整个生命周期内使用的多个线程 - 它是两个线程同时访问它,这将导致线程问题)。 您是否考虑过使用ConcurrentDictionary 而不是HashSet
  • @mjwills 感谢您的评论。我确实考虑过使用ConcurrentDictionary,但由于我实际上没有键值对,我决定坚持使用ISet
  • 您可以忽略这些值(或将它们设置为 null) - 在这种情况下,ConcurrentDictionary 基本上是 ConcurrentHashSet

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


【解决方案1】:

不保证每个 HTTP 请求都会在一个线程上完成。

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

这意味着您基于每个请求缓存的类不必是线程安全的,因为它们的状态不是并行访问的。它们确实需要能够从多个线程按顺序访问,但这通常只有在您开始存储线程 ID 或任何其他线程关联状态(例如使用 ThreadLocal&lt;T&gt;)时才会出现问题。

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

多个消费者依赖于这个 FooContext 并会调用 AddToSet/GetStrings 并根据结果运行不同的业务逻辑。

这些消费者可以依赖FooContext,只要他们的生命周期是TransientInstancePerRequest。这一直适用于他们的消费者在调用图上。如果你违反了这条规则,你会有一个Captive Dependency,这可能会导致你的FooContext实例被多个请求重用,从而导致并发问题。

不过,在多线程应用程序中使用 DI 时,您必须小心谨慎。 This documentation 确实给出了一些指示。

【讨论】:

  • 根本看不到调用代码,我们不可能说是否曾经从多个线程并行访问过该对象。当然,它可能不是,但也可能是,我们无从得知。
  • @Servy:我想说这个信息隐含在问题中。总会有一些编程错误导致FooContext 实例被缓存太久(例如因为Captive Dependency),但这不是问题所在。我们应该能够假设 OP 没有导致 FooContext 成为强制依赖。除非 OP 发布他的所有代码,否则我们永远无法确定他的代码没有问题。
  • @Servy,我也有同样的看法。这就是为什么我在这里问SO。 FooContext 类是由我编写的,截至今天,不会从多个线程调用调用者。但是我想将这个类包含在一个库中以便它可以被重用,这也意味着我无法预见未来的调用者如何访问它。我应该谨慎行事并仍然包括锁定吗?
  • @Servy:请重新阅读问题。问题是“我有一个注册为‘每个请求一个实例’的对象。我应该同步对它的访问吗?”这个问题的答案是否定的,你不必这样做。
  • @rexcfnghk:“我应该谨慎行事并仍然包括锁定吗?”这一切都取决于,但根据这个推理,您将在系统中的所有类中包含同步。这是适得其反的。相反,记录该类不是线程安全的。事实上,在 .NET 中,您应该始终假定对象是线程安全的,除非文档中明确指定。
【解决方案2】:

我没有在我的应用程序中明确创建新线程也没有调用 Task.Run(),但我确实使用了很多带有 ConfigureAwait(false) 的异步等待,这意味着继续可能在不同的线程上。

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

FooContext 不是线程安全的,这是真的吗?我上面的理解正确吗?

是的。

我会说调用代码实际上是错误的:ConfigureAwait(false) 表示“我不需要请求上下文”,但如果代码使用FooContext(请求上下文的一部分),那么它 确实需要请求上下文。

如果这确实是线程不安全的,并且我希望允许多个读取器但只允许一个独占写入器,我应该在 ISet 上应用 ReaderWriterLockSlim 吗?

几乎肯定不会。 lock 可能工作得很好。

仅仅因为某些代码执行读取而其他代码执行写入而使用读取器/写入器锁是一个常见的错误。仅当某些代码进行读取,其他代码进行写入并且存在大量并发读取访问时,才需要读取器/写入器锁。

【讨论】:

  • 你能否进一步解释一下为什么调用代码不应该在这里使用ConfigureAwait(false)?我看不出FooContext 是如何成为请求上下文的一部分
  • 它是隐式上下文的一部分。如果您认为“上下文”是指“任何可用的环境数据”,那么您不能只是跳到线程池线程并期望它工作。
  • 这是否意味着在 ASP.NET(不是 ASP.NET Core)Web API 应用程序的上下文中注册在 IoC 容器中的所有可等待依赖项不应使用 ConfigureAwait(false) 作为等待好吗?
  • 我问这个的原因是因为我的大部分依赖项也以与FooContext类似的方式注册在我的组合根目录中(其中大部分是InstancePerRequest)。
  • @rexcfnghk:任何非线程安全的组件一次只能从一个线程访问。如果ConfigureAwait(false) 导致了多线程访问,那就有问题了。
猜你喜欢
  • 2017-03-07
  • 1970-01-01
  • 2015-12-12
  • 1970-01-01
  • 2020-11-02
  • 1970-01-01
  • 2011-02-24
  • 1970-01-01
  • 2023-01-31
相关资源
最近更新 更多