这是任务:我需要根据文件名锁定。最多可以有一百万个不同的文件名。 (这用于大规模基于磁盘的缓存)。 我希望内存使用率低,查找时间短,这意味着我需要一个GC锁定字典。 (dict中只能存在使用中的锁定。)
回调操作可能需要几分钟才能完成,因此全局锁定是不可接受的。高吞吐量至关重要。
我在下面发布了我当前的解决方案,但我对复杂性感到不满。
编辑:请不要发布不是100%正确的解决方案。例如,允许在“获取锁定对象”阶段和“锁定”阶段之间从字典中删除锁定的解决方案是不正确的,无论它是否是“已接受”的设计模式。
有比这更优雅的解决方案吗?
谢谢!
[编辑:我根据RobV的建议更新了我的代码以使用循环与递归]
[编辑:再次更新代码以允许'超时'和更简单的呼叫模式。这可能是我使用的最终代码。仍然与原始帖子中的基本算法相同。]
[编辑:再次更新代码以处理没有孤立锁定对象的回调内的异常]
public delegate void LockCallback();
/// <summary>
/// Provides locking based on a string key.
/// Locks are local to the LockProvider instance.
/// The class handles disposing of unused locks. Generally used for
/// coordinating writes to files (of which there can be millions).
/// Only keeps key/lock pairs in memory which are in use.
/// Thread-safe.
/// </summary>
public class LockProvider {
/// <summary>
/// The only objects in this collection should be for open files.
/// </summary>
protected Dictionary<String, Object> locks =
new Dictionary<string, object>(StringComparer.Ordinal);
/// <summary>
/// Synchronization object for modifications to the 'locks' dictionary
/// </summary>
protected object createLock = new object();
/// <summary>
/// Attempts to execute the 'success' callback inside a lock based on 'key'. If successful, returns true.
/// If the lock cannot be acquired within 'timoutMs', returns false
/// In a worst-case scenario, it could take up to twice as long as 'timeoutMs' to return false.
/// </summary>
/// <param name="key"></param>
/// <param name="success"></param>
/// <param name="failure"></param>
/// <param name="timeoutMs"></param>
public bool TryExecute(string key, int timeoutMs, LockCallback success){
//Record when we started. We don't want an infinite loop.
DateTime startedAt = DateTime.UtcNow;
// Tracks whether the lock acquired is still correct
bool validLock = true;
// The lock corresponding to 'key'
object itemLock = null;
try {
//We have to loop until we get a valid lock and it stays valid until we lock it.
do {
// 1) Creation/aquire phase
lock (createLock) {
// We have to lock on dictionary writes, since otherwise
// two locks for the same file could be created and assigned
// at the same time. (i.e, between TryGetValue and the assignment)
if (!locks.TryGetValue(key, out itemLock))
locks[key] = itemLock = new Object(); //make a new lock!
}
// Loophole (part 1):
// Right here - this is where another thread (executing part 2) could remove 'itemLock'
// from the dictionary, and potentially, yet another thread could
// insert a new value for 'itemLock' into the dictionary... etc, etc..
// 2) Execute phase
if (System.Threading.Monitor.TryEnter(itemLock, timeoutMs)) {
try {
// May take minutes to acquire this lock.
// Trying to detect an occurence of loophole above
// Check that itemLock still exists and matches the dictionary
lock (createLock) {
object newLock = null;
validLock = locks.TryGetValue(key, out newLock);
validLock = validLock && newLock == itemLock;
}
// Only run the callback if the lock is valid
if (validLock) {
success(); // Extremely long-running callback, perhaps throwing exceptions
return true;
}
} finally {
System.Threading.Monitor.Exit(itemLock);//release lock
}
} else {
validLock = false; //So the finally clause doesn't try to clean up the lock, someone else will do that.
return false; //Someone else had the lock, they can clean it up.
}
//Are we out of time, still having an invalid lock?
if (!validLock && Math.Abs(DateTime.UtcNow.Subtract(startedAt).TotalMilliseconds) > timeoutMs) {
//We failed to get a valid lock in time.
return false;
}
// If we had an invalid lock, we have to try everything over again.
} while (!validLock);
} finally {
if (validLock) {
// Loophole (part 2). When loophole part 1 and 2 cross paths,
// An lock object may be removed before being used, and be orphaned
// 3) Cleanup phase - Attempt cleanup of lock objects so we don't
// have a *very* large and slow dictionary.
lock (createLock) {
// TryEnter() fails instead of waiting.
// A normal lock would cause a deadlock with phase 2.
// Specifying a timeout would add great and pointless overhead.
// Whoever has the lock will clean it up also.
if (System.Threading.Monitor.TryEnter(itemLock)) {
try {
// It succeeds, so no-one else is working on it
// (but may be preparing to, see loophole)
// Only remove the lock object if it
// still exists in the dictionary as-is
object existingLock = null;
if (locks.TryGetValue(key, out existingLock)
&& existingLock == itemLock)
locks.Remove(key);
} finally {
// Remove the lock
System.Threading.Monitor.Exit(itemLock);
}
}
}
}
}
// Ideally the only objects in 'locks' will be open operations now.
return true;
}
}
用法示例
LockProvider p = new LockProvider();
bool success = p.TryExecute("filename",1000,delegate(){
//This code executes within the lock
});
答案 0 :(得分:1)
根据你对文件的处理方式(你说基于磁盘的缓存,所以我假设读取和写入)然后我建议尝试基于ReaderWriterLock的东西,如果你可以升级到.Net 3.5那么尝试ReaderWriterLockSlim,因为它的表现要好得多。
作为减少示例中潜在的无限递归情况的一般步骤,将代码的第一位更改为以下内容:
do
{
// 1) Creation/aquire phase
lock (createLock){
// We have to lock on dictionary writes, since otherwise
// two locks for the same file could be created and assigned
// at the same time. (i.e, between TryGetValue and the assignment)
if (!locks.TryGetValue(key, out itemLock))
locks[key] = itemLock = new Object(); //make a new lock!
}
// Loophole (part 1):
// Right here - this is where another thread could remove 'itemLock'
// from the dictionary, and potentially, yet another thread could
// insert a new value for 'itemLock' into the dictionary... etc, etc..
// 2) Execute phase
lock(itemLock){
// May take minutes to acquire this lock.
// Real version would specify a timeout and a failure callback.
// Trying to detect an occurence of loophole above
// Check that itemLock still exists and matches the dictionary
lock(createLock){
object newLock = null;
validLock = locks.TryGetValue(key, out newLock);
validLock = validLock && newLock == itemLock;
}
// Only run the callback if the lock is valid
if (validLock) callback(); // Extremely long-running callback.
}
// If we had an invalid lock, we have to try everything over again.
} while (!validLock);
这将使用循环替换您的递归,从而避免无限递归导致StackOverflow的任何可能性。
答案 1 :(得分:1)
该解决方案肯定看起来很脆弱和复杂。在锁内部进行公共回调是不好的做法。为什么不让LockProvider
返回某种“锁定”对象,以便消费者自己进行锁定。这将locks
字典的锁定与执行分开。它可能看起来像这样:
public class LockProvider
{
private readonly object globalLock = new object();
private readonly Dictionary<String, Locker> locks =
new Dictionary<string, Locker>(StringComparer.Ordinal);
public IDisposable Enter(string key)
{
Locker locker;
lock (this.globalLock)
{
if (!this.locks.TryGetValue(key, out locker))
{
this.locks[key] = locker = new Locker(this, key);
}
// Increase wait count ínside the global lock
locker.WaitCount++;
}
// Call Enter and decrease wait count óutside the
// global lock (to prevent deadlocks).
locker.Enter();
// Only one thread will be here at a time for a given locker.
locker.WaitCount--;
return locker;
}
private sealed class Locker : IDisposable
{
private readonly LockProvider provider;
private readonly string key;
private object keyLock = new object();
public int WaitCount;
public Locker(LockProvider provider, string key)
{
this.provider = provider;
this.key = key;
}
public void Enter()
{
Monitor.Enter(this.keyLock);
}
public void Dispose()
{
if (this.keyLock != null)
{
this.Exit();
this.keyLock = null;
}
}
private void Exit()
{
lock (this.provider.globalLock)
{
try
{
// Remove the key before releasing the lock, but
// only when no threads are waiting (because they
// will have a reference to this locker).
if (this.WaitCount == 0)
{
this.provider.locks.Remove(this.key);
}
}
finally
{
// Release the keyLock inside the globalLock.
Monitor.Exit(this.keyLock);
}
}
}
}
}
LockProvider
可以按如下方式使用:
public class Consumer
{
private LockProvider provider;
public void DoStufOnFile(string fileName)
{
using (this.provider.Enter(fileName))
{
// Long running operation on file here.
}
}
}
注意{<1}}在之前被称为我们输入Monitor.Enter
语句(使用),这意味着在某些主机环境(例如ASP.NET和SQL Server)中我们当异步异常发生时,有可能永远不会释放锁。 ASP.NET和SQL Server等主机在发生超时时会主动杀死线程。使用try
内的Monitor.Enter
之外的Enter重写此内容有点棘手。
我希望这会有所帮助。
答案 2 :(得分:0)
你能不能简单地使用一个名为Mutex的名字来自你的文件名吗?
虽然不是轻量级同步原语,但它比管理自己的同步字典更简单。
但是,如果你真的想这样做,我认为以下实现看起来更简单。如果您使用的是.NET 3.5或更低版本,则需要同步字典 - .NET 4 ConcurrentDictionary
或您自己的实现。
try
{
object myLock = new object();
lock(myLock)
{
object otherLock = null;
while(otherLock != myLock)
{
otherLock = lockDictionary.GetOrAdd(key, myLock);
if (otherLock != myLock)
{
// Another thread has a lock in the dictionary
if (Monitor.TryEnter(otherLock, timeoutMs))
{
// Another thread still has a lock after a timeout
failure();
return;
}
else
{
Monitor.Exit(otherLock);
}
}
}
// We've successfully added myLock to the dictionary
try
{
// Do our stuff
success();
}
finally
{
lockDictionary.Remove(key);
}
}
}
答案 3 :(得分:0)
在.NET中似乎没有一种优雅的方法可以做到这一点,尽管我已经通过@ RobV建议的循环改进了算法。这是我决定采用的最终解决方案。
它不受'孤儿参考'的影响,似乎是典型的标准模式,随后是@Steven的答案。
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace ImageResizer.Plugins.DiskCache {
public delegate void LockCallback();
/// <summary>
/// Provides locking based on a string key.
/// Locks are local to the LockProvider instance.
/// The class handles disposing of unused locks. Generally used for
/// coordinating writes to files (of which there can be millions).
/// Only keeps key/lock pairs in memory which are in use.
/// Thread-safe.
/// </summary>
public class LockProvider {
/// <summary>
/// The only objects in this collection should be for open files.
/// </summary>
protected Dictionary<String, Object> locks =
new Dictionary<string, object>(StringComparer.Ordinal);
/// <summary>
/// Synchronization object for modifications to the 'locks' dictionary
/// </summary>
protected object createLock = new object();
/// <summary>
/// Attempts to execute the 'success' callback inside a lock based on 'key'. If successful, returns true.
/// If the lock cannot be acquired within 'timoutMs', returns false
/// In a worst-case scenario, it could take up to twice as long as 'timeoutMs' to return false.
/// </summary>
/// <param name="key"></param>
/// <param name="success"></param>
/// <param name="failure"></param>
/// <param name="timeoutMs"></param>
public bool TryExecute(string key, int timeoutMs, LockCallback success){
//Record when we started. We don't want an infinite loop.
DateTime startedAt = DateTime.UtcNow;
// Tracks whether the lock acquired is still correct
bool validLock = true;
// The lock corresponding to 'key'
object itemLock = null;
try {
//We have to loop until we get a valid lock and it stays valid until we lock it.
do {
// 1) Creation/aquire phase
lock (createLock) {
// We have to lock on dictionary writes, since otherwise
// two locks for the same file could be created and assigned
// at the same time. (i.e, between TryGetValue and the assignment)
if (!locks.TryGetValue(key, out itemLock))
locks[key] = itemLock = new Object(); //make a new lock!
}
// Loophole (part 1):
// Right here - this is where another thread (executing part 2) could remove 'itemLock'
// from the dictionary, and potentially, yet another thread could
// insert a new value for 'itemLock' into the dictionary... etc, etc..
// 2) Execute phase
if (System.Threading.Monitor.TryEnter(itemLock, timeoutMs)) {
try {
// May take minutes to acquire this lock.
// Trying to detect an occurence of loophole above
// Check that itemLock still exists and matches the dictionary
lock (createLock) {
object newLock = null;
validLock = locks.TryGetValue(key, out newLock);
validLock = validLock && newLock == itemLock;
}
// Only run the callback if the lock is valid
if (validLock) {
success(); // Extremely long-running callback, perhaps throwing exceptions
return true;
}
} finally {
System.Threading.Monitor.Exit(itemLock);//release lock
}
} else {
validLock = false; //So the finally clause doesn't try to clean up the lock, someone else will do that.
return false; //Someone else had the lock, they can clean it up.
}
//Are we out of time, still having an invalid lock?
if (!validLock && Math.Abs(DateTime.UtcNow.Subtract(startedAt).TotalMilliseconds) > timeoutMs) {
//We failed to get a valid lock in time.
return false;
}
// If we had an invalid lock, we have to try everything over again.
} while (!validLock);
} finally {
if (validLock) {
// Loophole (part 2). When loophole part 1 and 2 cross paths,
// An lock object may be removed before being used, and be orphaned
// 3) Cleanup phase - Attempt cleanup of lock objects so we don't
// have a *very* large and slow dictionary.
lock (createLock) {
// TryEnter() fails instead of waiting.
// A normal lock would cause a deadlock with phase 2.
// Specifying a timeout would add great and pointless overhead.
// Whoever has the lock will clean it up also.
if (System.Threading.Monitor.TryEnter(itemLock)) {
try {
// It succeeds, so no-one else is working on it
// (but may be preparing to, see loophole)
// Only remove the lock object if it
// still exists in the dictionary as-is
object existingLock = null;
if (locks.TryGetValue(key, out existingLock)
&& existingLock == itemLock)
locks.Remove(key);
} finally {
// Remove the lock
System.Threading.Monitor.Exit(itemLock);
}
}
}
}
}
// Ideally the only objects in 'locks' will be open operations now.
return true;
}
}
}
使用此代码非常简单:
LockProvider p = new LockProvider();
bool success = p.TryExecute("filename",1000,delegate(){
//This code executes within the lock
});