2010-11-19 26 views
28

我需要通过字符串锁定一段代码。当然下面的代码是不可靠的:按字符串锁定。这是安全/理智吗?

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

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

public class StringLock 
{ 
    private readonly Dictionary<string, LockObject> keyLocks = new Dictionary<string, LockObject>(); 
    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<string, LockObject> keyLocks = new Dictionary<string, LockObject>(); 
    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 
} 
+2

它看起来过于专注于我,但很难说不知道它应该解决什么问题。这只是为了避免锁定在一个字符串? – LukeH 2010-11-19 12:27:51

+0

@LukeH,我试图写一个图像缓存多个客户端(超过100左右)可能同时请求图像。第一次命中将请求来自另一台服务器的映像,并且我希望缓存中的所有其他请求都被阻止,直到此操作完成,以便它们可以检索缓存的版本。在我看来,这将需要锁定这种性质,但如果有其他选择,我全都是耳朵。 – spender 2010-11-19 12:36:17

+0

@Ani,是的,我知道这一点(正如我在文章中所述),如果我认为这种方法是一个游戏者,我会支撑防御。 – spender 2010-11-19 12:37:11

回答

10

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

但是,如果代价过高,只需要为每个活动网址添加一个锁定,上述内容仅值得一试。

+2

该代码的一大优点是它的实现非常简单,因为与基于字典的代码不同,它不需要移除锁定对象以防止内存泄漏。这是这个主题中唯一的代码,我有信心在没有太多想法的情况下正确实施。当然,在一个线程中同时持有多个锁定会变得非常难看。 – CodesInChaos 2010-11-19 14:55:49

+0

谢谢兄弟!我知道这就像4年后,但你刚刚帮助我想出一个很好的解决方案来解决我遇到的问题。 :) – Haney 2014-04-22 01:43:33

+0

@伊恩Ringrose你可以请提供一个代码示例? – 2017-02-10 06:44:38

14

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

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

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

这更简单;你测试的代码更少,因为你会依赖框架中已经存在的东西(据推测,它已经可以工作)。

+1

Monitor.Enter不锁定实例。该实例只是用于实现同步协议的令牌。 – 2010-11-19 13:38:38

+0

这正是*正是* OP想要避免的。如果另一个线程碰巧正在运行来自另一个模块的代码,这个模块决定锁定相同的实际字符串(巧合),那么就会有一个完全不必要的争用。 OP正试图为用户提供字符串锁定的容易性,但是没有.NET实习生池的阻碍。 – Ani 2010-11-19 14:30:49

+3

和内部泄漏内存 – CodesInChaos 2010-11-19 14:40:51

2

大约2年前我曾来实现同样的事情:

我想写的图像缓存 其中多个客户端(超过100或 左右)很可能会请求图像 同时。第一次点击将 请求来自另一个服务器的图像, ,我希望所有其他请求到 缓存阻止,直到此操作完成 ,以便它们可以检索到 缓存版本。

我结束了与你的代码(LockObjects字典)相同的代码。那么,你的封装似乎更好。

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

  1. 如果你需要更好的peformance它也许有用利用某种ReadWriteLock中的,因为你有100个读者仅1作家从另一台服务器获取图像。

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

+0

这意味着我的第二点是相关的,如果你依靠进入临界区的第一个线程与创建锁的相同。例如,如果它是第一个触发从其他服务器获取图像的线程。 – Adesit 2010-11-19 13:55:24

2

你已经创建了一个简单的锁语句的复杂包装。创建一个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); 
} 

如果原始最终代码抛出一个异常,那么您将留下一个无效的LockObject状态。

+0

这段代码应该抛出的唯一例外是像StackOverflow,OutOfMemory和线程流产这样的异步异常,在这种情况下,您应该卸载appdomain,所以它并不重要。 – CodesInChaos 2010-11-19 16:38:11

-1

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

例如,在蟒蛇:

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) 
+0

OP要求C#解决方案 – JJS 2016-08-30 17:22:59

-1

现在我有同样的问题。我决定用简单的解决办法:避免死锁创建具有固定唯一的前缀一个新的字符串,并把它作为一个锁对象:

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

这比不确保它是被实施的可怕性要小一些,但仍然是一个非常糟糕的主意。您不应该锁定公开可用的数据;你应该锁定只能被应该锁定的代码块访问的数据。 – Servy 2013-10-29 17:41:16

5

下面是一个非常简单的,优雅和纠正.NET 4的解决方案使用ConcurrentDictionary改编自this question

public static class StringLocker 
{ 
    private static readonly ConcurrentDictionary<string, object> _locks = new ConcurrentDictionary<string, object>(); 

    public static void DoAction(string s, Action action) 
    { 
     lock(_locks.GetOrAdd(s, new object())) 
     { 
      action(); 
     } 
    } 
} 

您可以使用此像这样:

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

此实现永远不会回收未使用的锁,并且最终会在长时间运行的过程中消耗大量内存。 – CaseyB 2014-07-21 19:23:58

+0

@CaseyB锁将在'lock'代码块的末尾回收。我认为你的意思是,未使用的*锁对象*将保留在'_locks'字典中。这是真的,但内存使用量将与URL的数量成正比......并且如果你做了一些快速计算,你会发现URL的数量必须非常大以使内存成为问题。 – 2014-07-21 22:53:16

+0

@ZaidMasud:这是一个有效的想法,但也许CaseyB有一点......它可以通过在try-finally中包装'action();并在finally子句中从_locks中删除字符串条目来解决I – digEmAll 2017-06-08 13:11:27

0

我加入由@尤金 - beresovsky answer启发Bardock.Utils封装解决方案。

用法:

private static LockeableObjectFactory<string> _lockeableStringFactory = 
    new LockeableObjectFactory<string>(); 

string key = ...; 

lock (_lockeableStringFactory.Get(key)) 
{ 
    ... 
} 

Solution code

namespace Bardock.Utils.Sync 
{ 
    /// <summary> 
    /// 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 
    /// </summary> 
    /// <typeparam name="TSeed">Type of the object you want lock on</typeparam> 
    public class LockeableObjectFactory<TSeed> 
    { 
     private readonly ConcurrentDictionary<TSeed, object> _lockeableObjects = new ConcurrentDictionary<TSeed, object>(); 

     /// <summary> 
     /// Creates or uses an existing object instance by specified seed 
     /// </summary> 
     /// <param name="seed"> 
     /// The object used to generate a new lockeable object. 
     /// The default EqualityComparer<TSeed> is used to determine if two seeds are equal. 
     /// The same object instance is returned for equal seeds, otherwise a new object is created. 
     /// </param> 
     public object Get(TSeed seed) 
     { 
      return _lockeableObjects.GetOrAdd(seed, valueFactory: x => new object()); 
     } 
    } 
} 
0

这是我如何实现这种锁定模式:

public class KeyLocker<TKey> 
{ 
    private class KeyLock 
    { 
     public int Count; 
    } 

    private readonly Dictionary<TKey, KeyLock> keyLocks = new Dictionary<TKey, KeyLock>(); 

    public T ExecuteSynchronized<T>(TKey key, Func<TKey, T> 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<TKey> action) 
    { 
     this.ExecuteSynchronized<bool>(key, k => 
     { 
      action(k); 
      return true; 
     }); 
    } 
} 

而且像这样使用:

private locker = new KeyLocker<string>(); 
...... 

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