我想找出ImageProcessor库在此处引起的问题,在向缓存中添加项目时出现间歇性的文件访问错误。
System.IO.IOException:该进程无法访问文件'D:\ home \ site \ wwwroot \ app_data \ cache \ 0 \ 6 \ 5 \ f \ 2 \ 7 \ 065f27fc2c8e843443d210a1e84d1ea28bbab6c4.webp',因为该文件正在由另一个进程使用。
我编写了一个类,旨在根据散列url生成的密钥执行异步锁定,但是看来我在实现中错过了一些东西。
我的锁课
public sealed class AsyncDuplicateLock
{
/// <summary>
/// The collection of semaphore slims.
/// </summary>
private static readonly ConcurrentDictionary<object, SemaphoreSlim> SemaphoreSlims
= new ConcurrentDictionary<object, SemaphoreSlim>();
/// <summary>
/// Locks against the given key.
/// </summary>
/// <param name="key">
/// The key that identifies the current object.
/// </param>
/// <returns>
/// The disposable <see cref="Task"/>.
/// </returns>
public IDisposable Lock(object key)
{
DisposableScope releaser = new DisposableScope(
key,
s =>
{
SemaphoreSlim locker;
if (SemaphoreSlims.TryRemove(s, out locker))
{
locker.Release();
locker.Dispose();
}
});
SemaphoreSlim semaphore = SemaphoreSlims.GetOrAdd(key, new SemaphoreSlim(1, 1));
semaphore.Wait();
return releaser;
}
/// <summary>
/// Asynchronously locks against the given key.
/// </summary>
/// <param name="key">
/// The key that identifies the current object.
/// </param>
/// <returns>
/// The disposable <see cref="Task"/>.
/// </returns>
public Task<IDisposable> LockAsync(object key)
{
DisposableScope releaser = new DisposableScope(
key,
s =>
{
SemaphoreSlim locker;
if (SemaphoreSlims.TryRemove(s, out locker))
{
locker.Release();
locker.Dispose();
}
});
Task<IDisposable> releaserTask = Task.FromResult(releaser as IDisposable);
SemaphoreSlim semaphore = SemaphoreSlims.GetOrAdd(key, new SemaphoreSlim(1, 1));
Task waitTask = semaphore.WaitAsync();
return waitTask.IsCompleted
? releaserTask
: waitTask.ContinueWith(
(_, r) => (IDisposable)r,
releaser,
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
}
/// <summary>
/// The disposable scope.
/// </summary>
private sealed class DisposableScope : IDisposable
{
/// <summary>
/// The key
/// </summary>
private readonly object key;
/// <summary>
/// The close scope action.
/// </summary>
private readonly Action<object> closeScopeAction;
/// <summary>
/// Initializes a new instance of the <see cref="DisposableScope"/> class.
/// </summary>
/// <param name="key">
/// The key.
/// </param>
/// <param name="closeScopeAction">
/// The close scope action.
/// </param>
public DisposableScope(object key, Action<object> closeScopeAction)
{
this.key = key;
this.closeScopeAction = closeScopeAction;
}
/// <summary>
/// Disposes the scope.
/// </summary>
public void Dispose()
{
this.closeScopeAction(this.key);
}
}
}
用法-在HttpModule中
private readonly AsyncDuplicateLock locker = new AsyncDuplicateLock();
using (await this.locker.LockAsync(cachedPath))
{
// Process and save a cached image.
}
谁能发现我哪里出问题了?我担心我误解了一些基本知识。
该库的完整源代码存储在此处的Github上
正如另一个回答者所指出的那样,原始代码是在释放信号量之前SemaphoreSlim
从中删除ConcurrentDictionary
。因此,你有太多的信号量搅动-当它们仍然可以使用时(它们尚未被获取,但是已经从字典中检索出来),它们将从字典中删除。
这种“映射锁”的问题在于,很难知道何时不再需要该信号量。一种选择是根本不丢弃信号灯。那是简单的解决方案,但在你的情况下可能不可接受。如果信号量实际上与对象实例相关,而不与值(如字符串)相关,则另一种选择是使用星历表将它们附加在一起。但是,我相信在你的情况下此选项也不可接受。
因此,我们很难做到这一点。:)
有几种可行的方法。我认为从引用计数的角度(对字典中的每个信号量进行引用计数)来看待它是有意义的。另外,我们希望使减数计数和删除操作成为原子操作,所以我只使用一个lock
(使并发字典变得多余):
public sealed class AsyncDuplicateLock
{
private sealed class RefCounted<T>
{
public RefCounted(T value)
{
RefCount = 1;
Value = value;
}
public int RefCount { get; set; }
public T Value { get; private set; }
}
private static readonly Dictionary<object, RefCounted<SemaphoreSlim>> SemaphoreSlims
= new Dictionary<object, RefCounted<SemaphoreSlim>>();
private SemaphoreSlim GetOrCreate(object key)
{
RefCounted<SemaphoreSlim> item;
lock (SemaphoreSlims)
{
if (SemaphoreSlims.TryGetValue(key, out item))
{
++item.RefCount;
}
else
{
item = new RefCounted<SemaphoreSlim>(new SemaphoreSlim(1, 1));
SemaphoreSlims[key] = item;
}
}
return item.Value;
}
public IDisposable Lock(object key)
{
GetOrCreate(key).Wait();
return new Releaser { Key = key };
}
public async Task<IDisposable> LockAsync(object key)
{
await GetOrCreate(key).WaitAsync().ConfigureAwait(false);
return new Releaser { Key = key };
}
private sealed class Releaser : IDisposable
{
public object Key { get; set; }
public void Dispose()
{
RefCounted<SemaphoreSlim> item;
lock (SemaphoreSlims)
{
item = SemaphoreSlims[Key];
--item.RefCount;
if (item.RefCount == 0)
SemaphoreSlims.Remove(Key);
}
item.Value.Release();
}
}
}
“是用星历表附加它们”,您在这里迷失了我,您能解释一下这是什么意思吗?
星历表是一种动态的语言概念,它将一个对象与另一个对象的生命周期联系在一起。就像可以添加到的属性一样
ExpandoObject
,但星历表可以附加到任何对象(在这方面更像JavaScript属性)。唯一的.NET星历表是ConditionalWeakTable
难以使用的对象。我编写了一个简单的包装库,称为ConnectedProperties。这太棒了!我将永远尝试过度设计的一种优雅方法。我得到了一个从未部署过的实现,但是对于不断增加的内存使用情况我真的不满意。非常感谢!
@太:我不会。锁定下花费的时间非常短,因此,即使大多数使用都被读取,也可能不值得更改。
@boris:我几乎从不使用“尝试锁”;对我来说,这表明某处可能不匹配。因此,我的第一个建议是退后一步,放眼大局,看看是否有完全不同的方法更有意义。这就是说,你可以做“试锁”与
SemaphoreSlim
-呼叫WaitAsync(0)
。棘手的部分是您将要公开的API。一个可以为null的值IDisposable
是一个选项(null
表示未获取锁定)。