2010-11-19 25 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()); 
}