按字符串锁定。 这样安全/理智吗?

我需要按字符串锁定一段代码。 当然,以下代码非常不安全:

lock("http://someurl") { //bla } 

所以我一直在做一个替代品。 我通常不会在这里发布大量的代码,但是当涉及到并发编程时,我有点担心我自己的同步方案,所以我提交我的代码来询问是否理智这样做以这种方式或是否有一个更直接的方法。

 public class StringLock { private readonly Dictionary keyLocks = new Dictionary(); private readonly object keyLocksLock = new object(); public void LockOperation(string url, Action action) { LockObject obj; lock (keyLocksLock) { if (!keyLocks.TryGetValue(url, out obj)) { keyLocks[url] = obj = new LockObject(); } obj.Withdraw(); } Monitor.Enter(obj); try { action(); } finally { lock (keyLocksLock) { if (obj.Return()) { keyLocks.Remove(url); } Monitor.Exit(obj); } } } private class LockObject { private int leaseCount; public void Withdraw() { Interlocked.Increment(ref leaseCount); } public bool Return() { return Interlocked.Decrement(ref leaseCount) == 0; } } } 

我会像这样使用它:

 StringLock.LockOperation("http://someurl",()=>{ //bla }); 

很高兴,或崩溃和燃烧?

编辑

对于后代,这是我的工作代码。 感谢所有的建议:

 public class StringLock { private readonly Dictionary keyLocks = new Dictionary(); private readonly object keyLocksLock = new object(); public IDisposable AcquireLock(string key) { LockObject obj; lock (keyLocksLock) { if (!keyLocks.TryGetValue(key, out obj)) { keyLocks[key] = obj = new LockObject(key); } obj.Withdraw(); } Monitor.Enter(obj); return new DisposableToken(this, obj); } private void ReturnLock(DisposableToken disposableLock) { var obj = disposableLock.LockObject; lock (keyLocksLock) { if (obj.Return()) { keyLocks.Remove(obj.Key); } Monitor.Exit(obj); } } private class DisposableToken : IDisposable { private readonly LockObject lockObject; private readonly StringLock stringLock; private bool disposed; public DisposableToken(StringLock stringLock, LockObject lockObject) { this.stringLock = stringLock; this.lockObject = lockObject; } public LockObject LockObject { get { return lockObject; } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } ~DisposableToken() { Dispose(false); } private void Dispose(bool disposing) { if (disposing && !disposed) { stringLock.ReturnLock(this); disposed = true; } } } private class LockObject { private readonly string key; private int leaseCount; public LockObject(string key) { this.key = key; } public string Key { get { return key; } } public void Withdraw() { Interlocked.Increment(ref leaseCount); } public bool Return() { return Interlocked.Decrement(ref leaseCount) == 0; } } } 

使用如下:

 var stringLock=new StringLock(); //... using(stringLock.AcquireLock(someKey)) { //bla } 

另一种选择是获取每个URL的HashCode,然后将其除以素数,并将其用作锁数组的索引。 这将限制您需要的锁定数量,同时通过选择要使用的锁定数量来控制“错误锁定”的可能性。

但是,如果成本太高,每个活动url只有一个锁,那么上述情况是值得的。

锁定任意字符串实例将是一个坏主意,因为Monitor.Lock锁定实例。 如果您有两个具有相同内容的不同字符串实例,那么这将是两个您不想要的独立锁。 所以你关心任意字符串是正确的。

但是,.NET已经有一个内置机制来返回给定字符串内容的“规范实例”: String.Intern 。 如果您传递两个具有相同内容的不同字符串实例,则两次都会返回相同的结果实例。

 lock (string.Intern(url)) { ... } 

这更简单; 您可以使用较少的代码进行测试,因为您将依赖于框架中已有的内容(可能已经有效)。

这是一个非常简单 ,优雅和正确的.NET 4解决方案,使用ConcurrentDictionary改编自这个问题 。

 public static class StringLocker { private static readonly ConcurrentDictionary _locks = new ConcurrentDictionary(); public static void DoAction(string s, Action action) { lock(_locks.GetOrAdd(s, new object())) { action(); } } } 

您可以这样使用:

 StringLocker.DoAction("http://someurl", () => { ... }); 

大约2年前,我必须实现同样的事情:

我正在尝试编写一个图像缓存,其中多个客户端(超过100个)可能同时请求图像。 第一个命中将从另一个服务器请求图像,并且我希望阻止所有其他对缓存的请求,直到此操作完成,以便他们可以检索缓存版本。

我最终得到的代码和你的代码一样(LockObjects的字典)。 好吧,你的封装似乎更好。

所以,我认为你有一个很好的解决方案。 只有2条评论:

  1. 如果你需要更好的性能,那么使用某种ReadWriteLock可能很有用,因为你有100个读者,只有1个写入者从另一个服务器获取图像。

  2. 我不确定在Monitor.Enter(obj)之前的线程切换情况会发生什么; 在你的LockOperation()中。 比如,第一个想要图像的线程构造一个新的锁,然后在它进入临界区之前进行线程切换。 然后可能发生第二个线程在第一个线程之前进入临界区域。 好吧,这可能不是一个真正的问题。

你已经围绕一个简单的锁语句创建了一个非常复杂的包装器。 创建一个url字典并为每个人创建一个锁定对象不是更好。 你可以干脆做。

 objLink = GetUrl("Url"); //Returns static reference to url/lock combination lock(objLink.LockObject){ //Code goes here } 

您甚至可以通过直接锁定objLink对象来简化这一过程,因为它可能是GetUrl(“Url”)字符串实例。 (你必须锁定静态字符串列表)

如果非常容易出错,那么您就是原始代码。 如果代码:

 if (obj.Return()) { keyLocks.Remove(url); } 

如果原始finally代码抛出exception,则会留下无效的LockObject状态。

我在@ eugene-beresovsky 回答的启发下添加了Bardock.Utils包中的解决方案。

用法:

 private static LockeableObjectFactory _lockeableStringFactory = new LockeableObjectFactory(); string key = ...; lock (_lockeableStringFactory.Get(key)) { ... } 

解决方案代码

 namespace Bardock.Utils.Sync { ///  /// Creates objects based on instances of TSeed that can be used to acquire an exclusive lock. /// Instanciate one factory for every use case you might have. /// Inspired by Eugene Beresovsky's solution: https://stackoverflow.com/a/19375402 ///  /// Type of the object you want lock on public class LockeableObjectFactory { private readonly ConcurrentDictionary _lockeableObjects = new ConcurrentDictionary(); ///  /// Creates or uses an existing object instance by specified seed ///  ///  /// The object used to generate a new lockeable object. /// The default EqualityComparer is used to determine if two seeds are equal. /// The same object instance is returned for equal seeds, otherwise a new object is created. ///  public object Get(TSeed seed) { return _lockeableObjects.GetOrAdd(seed, valueFactory: x => new object()); } } } 

这就是我实现这个锁定模式的方法:

 public class KeyLocker { private class KeyLock { public int Count; } private readonly Dictionary keyLocks = new Dictionary(); public T ExecuteSynchronized(TKey key, Func function) { KeyLock keyLock = null; try { lock (keyLocks) { if (!keyLocks.TryGetValue(key, out keyLock)) { keyLock = new KeyLock(); keyLocks.Add(key, keyLock); } keyLock.Count++; } lock (keyLock) { return function(key); } } finally { lock (keyLocks) { if (keyLock != null && --keyLock.Count == 0) keyLocks.Remove(key); } } } public void ExecuteSynchronized(TKey key, Action action) { this.ExecuteSynchronized(key, k => { action(k); return true; }); } } 

像这样使用:

 private locker = new KeyLocker(); ...... void UseLocker() { locker.ExecuteSynchronized("some string", () => DoSomething()); } 

在大多数情况下,当您认为需要锁定时,则不会。 相反,尝试使用线程安全的数据结构(例如Queue)来处理锁定。

例如,在python中:

 class RequestManager(Thread): def __init__(self): super().__init__() self.requests = Queue() self.cache = dict() def run(self): while True: url, q = self.requests.get() if url not in self.cache: self.cache[url] = urlopen(url).read()[:100] q.put(self.cache[url]) # get() runs on MyThread's thread, not RequestManager's def get(self, url): q = Queue(1) self.requests.put((url, q)) return q.get() class MyThread(Thread): def __init__(self): super().__init__() def run(self): while True: sleep(random()) url = ['http://www.google.com/', 'http://www.yahoo.com', 'http://www.microsoft.com'][randrange(0, 3)] img = rm.get(url) print(img) 

现在我有同样的问题。 我决定使用简单的解决方案:避免死锁创建一个具有固定唯一前缀的新字符串,并将其用作锁定对象:

 lock(string.Intern("uniqueprefix" + url)) { // }