使用后台服务的 .Net Core 中的缓存失败:"Adding the specified count to the semaphore would cause it to exceed its m



我已经实现了一个用于缓存的BackgroundService,完全按照微软在这里描述的步骤。我创建了默认的WebApi项目,并将Microsoft代码中的照片提取替换为生成WeatherForecast对象数组,因为它已经在示例项目中可用。我也删除了所有的HttpClient代码,包括DI的东西。

我配置了1分钟的间隔,当我运行代码时,立即命中CacheWorker.ExecuteAsync方法,所以一切正常。然后,1分钟后,我的断点再次被击中,只有当我击中Continue时,应用程序崩溃:

System.Threading.SemaphoreFullException: Adding the specified count to the semaphore would cause it to exceed its maximum count.
at System.Threading.SemaphoreSlim.Release(Int32 releaseCount)
at System.Threading.SemaphoreSlim.Release()
at WebApiForBackgroundService.CacheSignal`1.Release() in D:Devmy workWebApiForBackgroundServiceWebApiForBackgroundServiceCacheSignal.cs:line 18
at WebApiForBackgroundService.CacheWorker.ExecuteAsync(CancellationToken stoppingToken) in D:Devmy workWebApiForBackgroundServiceWebApiForBackgroundServiceCacheWorker.cs:line 61
at Microsoft.Extensions.Hosting.Internal.Host.TryExecuteBackgroundServiceAsync(BackgroundService backgroundService)
'WebApiForBackgroundService.exe' (CoreCLR: clrhost): Loaded 'C:Program FilesdotnetsharedMicrosoft.NETCore.App6.0.11Microsoft.Win32.Registry.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Microsoft.Extensions.Hosting.Internal.Host: Critical: The HostOptions.BackgroundServiceExceptionBehavior is configured to StopHost. A BackgroundService has thrown an unhandled exception, and the IHost instance is stopping. To avoid this behavior, configure this to Ignore; however the BackgroundService will not be restarted.

我的worker服务代码:

using Microsoft.Extensions.Caching.Memory;
namespace WebApiForBackgroundService;
public class CacheWorker : BackgroundService
{
private static readonly string[] Summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
private readonly CacheSignal<WeatherForecast> _cacheSignal;
private readonly IMemoryCache _cache;
public CacheWorker(
CacheSignal<WeatherForecast> cacheSignal,
IMemoryCache cache) =>
(_cacheSignal, _cache) = (cacheSignal, cache);
public override async Task StartAsync(CancellationToken cancellationToken)
{
await _cacheSignal.WaitAsync();
await base.StartAsync(cancellationToken);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
WeatherForecast[]? forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
_cache.Set("FORECASTS", forecasts);
}
finally
{
_cacheSignal.Release();
}
try
{
await Task.Delay(TimeSpan.FromMinutes(2), stoppingToken);
}
catch (OperationCanceledException)
{
break;
}
}
}
}

异常发生在调用_cacheSignal.Release()时,在第二次循环期间,它是由cachessignal类抛出的:

namespace WebApiForBackgroundService;
public class CacheSignal<T>
{
private readonly SemaphoreSlim _semaphore = new(1, 1);
public async Task WaitAsync() => await _semaphore.WaitAsync();
public void Release() => _semaphore.Release(); // THROWS EXCEPTION DURING 2ND LOOP
}

最后是my service:

using Microsoft.Extensions.Caching.Memory;
namespace WebApiForBackgroundService;
public sealed class WeatherService : IWeatherService
{
private readonly IMemoryCache _cache;
private readonly CacheSignal<WeatherForecast> _cacheSignal;
public WeatherService(
IMemoryCache cache,
CacheSignal<WeatherForecast> cacheSignal) =>
(_cache, _cacheSignal) = (cache, cacheSignal);
public async Task<List<WeatherForecast>> GetForecast()
{
try
{
await _cacheSignal.WaitAsync();
WeatherForecast[] forecasts =
(await _cache.GetOrCreateAsync(
"FORECASTS", _ =>
{
return Task.FromResult(Array.Empty<WeatherForecast>());
}))!;
return forecasts.ToList();
}
finally
{
_cacheSignal.Release();
}
}
}

这个例子似乎有问题。这个想法似乎是为了检查是否没有人在第一次设置缓存之前使用它,如文章本身所述:

重要:您需要覆盖BackgroundService.StartAsync并调用await_cacheSignal.WaitAsync(),以防止CacheWorker的启动和对PhotoService.GetPhotosAsync的调用之间的竞争条件。

试着将ExecuteAsync更改为以下内容:

public sealed class CacheWorker : BackgroundService
{
// ...
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var first = true;
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Updating cache.");
try
{
//...
}
finally
{
if(first)
{
first = false;
_cacheSignal.Release();
}
}
}
}
}

否则你将会有一个无尽的循环,它会尝试每次释放信号量,而它可以有最多1个槽(因此异常)。

链接:

  • repro with my fix
  • docs PR和PR与fix

在方法StartAsync中,您正在等待信号量。' private readonly SemaphoreSlim _semaphore = new(1,1); ' .

当您拥有它时,ExecuteAsync被调用。在该方法中,使用_cacheSignal.Release();释放信号量。然后等待1分钟,再次循环。信号量再次被释放。但是你已经释放了它一次,没有调用await _cacheSignal.WaitAsync();,这会导致崩溃。

你必须理解信号量的目的,并知道在哪里释放它是最好的,它在你发送的页面中说:

你需要…调用await _cachessignal . waitasync(),以防止在CacheWorker启动和调用PhotoService.GetPhotosAsync之间出现竞争条件。

或者,如果你只想要一个可以工作的代码,你可以这样做:

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_cacheSignal.Release(); // Releases the semaphore after CacheWorker is started.
while (!stoppingToken.IsCancellationRequested)
{
...
}
}

希望你明白我在说什么。

我已经设法创建了一个工作版本,从其他答案的提示(建议的解决方案似乎不起作用)。我的猜测是WaitAsync必须被称为每次在执行Release()之前,但这不会发生在循环的后续运行中。所以我已经删除了StartAsync中的调用,并将其添加到while循环中:

using Microsoft.Extensions.Caching.Memory;
namespace WebApiForBackgroundService;
public class CacheWorker : BackgroundService
{
private static readonly string[] Summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
private readonly CacheSignal<WeatherForecast> _cacheSignal;
private readonly IMemoryCache _cache;
public CacheWorker(
CacheSignal<WeatherForecast> cacheSignal,
IMemoryCache cache) =>
(_cacheSignal, _cache) = (cacheSignal, cache);
public override async Task StartAsync(CancellationToken cancellationToken)
{
await base.StartAsync(cancellationToken);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await _cacheSignal.WaitAsync();

try
{
WeatherForecast[]? forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
_cache.Set("FORECASTS", forecasts);
}
finally
{
_cacheSignal.Release();
}
try
{
await Task.Delay(TimeSpan.FromMinutes(2), stoppingToken);
}
catch (OperationCanceledException)
{
break;
}
}
}
}

这似乎有效。我想听听你的意见……

相关内容

最新更新