Blazor OwningComponentBase

[删除(380066935@qq.com或微信通知)]

更好的阅读体验请查看原文:https://blazor-university.com/dependency-injection/component-scoped-dependencies/owningcomponentbase-generic/

单例依赖项部分所述, 单一实例注册依赖项必须没有状态,或者应仅包含可在所有之间共享的状态 同一服务器上的用户。

而且,如作用域依赖项部分所述, 作用域内注册依赖项将单个用户的状态与其他所有人隔离开来 (甚至是同一用户在不同的浏览器选项卡中访问同一网站)。

但是线程安全呢? 运行服务器端应用程序时,很可能会使用单例注册依赖项 一次由多个线程。 即使我们将依赖项注册为 Scoped,也完全有可能由 不同的线程,这在多线程渲染部分中有详细说明。

因此,我们在编写服务时必须考虑线程安全性。 但是,有时我们不拥有我们使用的服务源代码,并且它们可能不是线程安全的 (一个例子是EntityFrameworkCore的类)。DbContext

演示问题

首先,我们将修改标准的服务器端 Blazor 项目,使其不再是线程安全的。 我们将检测是否多个线程同时使用该服务,并抛出一个 , 就像班级一样。WeatherForecastServiceInvalidOperationExceptionDbContext

我们将通过保留一个线程安全字段来实现这一点,我们可以在方法启动时递增该字段, 并随着方法的完成而递减。 如果我们尝试递增它时该值已经是,那么我们可以推断出另一个线程已经在执行 方法,然后引发异常。Int32> 0

创建项目后,编辑 /Data/WeatherForecastService.cs 文件并添加一个新字段:volatile int

私有易失性 int 锁定;

在方法的开头,我们将使用 Interlocked.CompareExchange 框架方法来确保该值当前为 ,然后将其从 更改为 。 在方法结束时,我们将使用 Interlocked.Decrement 将 back 的值更改为 。Locked001Locked0

我们还需要延迟方法, 否则,我们让两个线程同时执行它的机会太小了。 应更改为以下代码。GetForecastAsyc

public class WeatherForecastService
{
  private volatile int Locked;

  private static readonly string[] Summaries = new[]
  {
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
  };

  public async Task<WeatherForecast[]> GetForecastAsync(DateTime startDate)
  {
    if (Interlocked.CompareExchange(ref Locked, 1, 0) > 0)
      throw new InvalidOperationException(
        "A second operation started on this context before a previous operation completed. Any "
        + "instance members are not guaranteed to be thread-safe.");

    try
    {
      await Task.Delay(3000);
      var rng = new Random();
      return Enumerable.Range(1, 5).Select(index => new WeatherForecast
      {
        Date = startDate.AddDays(index),
        TemperatureC = rng.Next(-20, 55),
        Summary = Summaries[rng.Next(Summaries.Length)]
      }).ToArray();
    }
    finally
    {
      Interlocked.Decrement(ref Locked);
    }
  }
}
  • 第 3 行 添加了
    私有易失性 int 字段以跟踪当前正在执行相同方法的线程数。
  • 第 12
    行用于将值设置为 ,但前提是该值当前为 。 的原始值是从方法返回的,如果它大于,那么我们抛出一个.
    Interlocked.CompareExchangeLocked10Locked0InvalidOperationException
  • 第 19 行
    我们引入了 3 秒来模拟长时间运行的进程。 这将增加两个线程冲突的风险,并确保我们的代码实际异步运行。 (请参阅多线程渲染)。
    await Task.Delay
  • 第 30 行
    方法完成后, 将计数从回退到递减,以便另一个线程可以在没有的情况下执行该方法 收到异常。
    Locked10

现在运行应用程序,并尝试同时在两个浏览器选项卡中打开 /fetchdata 页面, 并且我们的“无效操作异常”应该被抛出。

修复 1:使用作用域依赖项

将依赖项更改为依赖项(在 Startup.cs 中)将阻止 用户中出现的线程重入问题。WeatherForecastServiceSingletonScoped

public void ConfigureServices(IServiceCollection services)
{
  services.AddRazorPages();
  services.AddServerSideBlazor();
  services.AddScoped<WeatherForecastService>();
}

再次运行应用程序,我们将看到我们能够打开许多选项卡而不会引起线程冲突。 如果已阅读有关作用域依赖项的部分, 原因就很明显了。 每个选项卡都接收其自己唯一的实例,因此只有一个线程使用每个 随时提供服务。WeatherForecastService

但是,我们只能保证我们的服务不会被我们应用程序的其他用户的线程使用。 它不能保证我们的组件根本不会被多个线程使用。 多线程呈现部分介绍了服务器端 Blazor 应用程序如何利用多个线程来呈现用户界面。

可能为单个用户呈现多个线程意味着我们仍然有可能进行线程重入 在多个组件之间共享的服务实例上。

如果无法使服务线程安全, 那么在这种情况下,一个选项是确保每个组件都注入了自己唯一的服务实例。

演示作用域依赖项的问题

创建一个名为 Conflict.Razor 的新页面,并为其提供以下简单标记。

@page "/conflict"

<FetchData />
<FetchData />

在我们的页面中呈现两次将需要两个组件来访问 , 并且因为该方法在其中有一个。 这意味着渲染第一个线程将能够在第一个线程完成之前继续渲染第二个线程。FetchDataWeatherForecastServiceGetForecastAsyncawait<FetchData/><FetchData/>

再次运行应用程序并导航到 /conflict 页面,我们将再次看到我们的 又扔了。InvalidOperationException

修复2:从拥有组件库下降

Blazor 有一个名为 的泛型组件类。 当创建此类的实例时,它将首先创建自己的实例(用于解析依赖项), 然后,将使用该服务提供商创建 (然后存储在名为 的属性中。OwningComponentBase<T>IServiceProviderTService

因为拥有自己独特的(因此得名), 这意味着来自服务提供商的解析对于我们的组件是唯一的。OwningComponentBase<T>IServiceProvider**Owning**ComponentBaseT

当我们的组件被处置时,它的(属性)也被处置, 反过来,它将处理它创建的每个实例 - 在这种情况下,我们的.OwningComponentBase<T>IServiceProviderServiceWeatherForecastService

拥有组件库服务器端托管

拥有组件库 Web 程序集托管

在 /Pages 中创建一个名为 OwnedFetchDataPage.razor 的新组件,并输入以下标记。

@page "/owned-fetchdata"
<OwnedFetchData/>
<OwnedFetchData />

在 /Shared 文件夹中,创建一个名为 OwnedFetchData.razor 的新组件,并复制标记 来自 FetchData.razor 文件。

目前,该组件具有相同的缺陷, 它注入了与当前其他组件共享的相同实例 浏览器选项卡。 若要解决此问题,请按照以下步骤操作。OwnedFetchDataWeatherForecastService

  1. 在页面顶部移除
    @inject WeatherForecastService ForecastService
  2. 将删除的行替换为
    @inherits OwningComponentBase<WeatherForecastService>
  3. 删除代码行
    forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
  4. 将其替换为
    forecasts = await Service.GetForecastAsync(DateTime.Now);

再次运行应用程序并导航到 /owned-fetchdata 页面。 因为组件的每个实例都拥有自己的 、 它们能够彼此独立地与服务交互,并且不会导致线程重入问题。OwnedFetchDataWeatherForecastService

如果我们的服务器端应用程序正在访问数据库, 我们可能会从数据库中下降我们的组件并从数据库中获取数据到一个数组中 渲染。OwningComponentBase<MyDbContext>