對Monitor的使用可以防止lock的時間過長並且可以設定其對應的超時時間達到對預期程式碼的一個控制,合理的使用timeout可以有助於程式的健壯性。但是對於不同的併發程式可能某些時候我們需要的粒度是不一樣的,從而產生的一個問題是需要更細粒度的鎖來保證,又因為預設的字串無法共享導致的無法通過string來進行鎖粒度的細分所以可能需要自己重寫一個鎖來保證到達更細粒度的控制,可能這時候有人要說不是有string.Intern可以進行字串保留,達到類似的效果,對於這個我只想說內部確定的字串確實可以達到這個效果,但是因為字串保留機制就導致了gc不會對此進行回收,會導致如果外部輸入的string是不可控的情況下就可以給程式造成諸如oom的問題,對此我拋磚引玉希望各位博友可以提出寶貴的意見或者建議,或者有現成的更好的解決方案也請分享一下,下面貼程式碼:
public class MonitorStr { private MonitorStr() { } private static ConcurrentDictionary<string, MonitorStrEntry> _lockDics = new ConcurrentDictionary<string, MonitorStrEntry>(); private const int _concurrentCount = 31; private static object[] _lockers = new object[_concurrentCount]; static MonitorStr() { for (int i = 0; i < _concurrentCount; i++) { _lockers[i] = new object(); } } private static int GetIndex(string key) { return Math.Abs(key.GetHashCode() % _concurrentCount); } public static bool TryEnter(string key, int timeoutMillis) { if (string.IsNullOrWhiteSpace(key)) throw new ArgumentNullException(nameof(key)); MonitorStrEntry entry = null; var locker = _lockers[GetIndex(key)]; lock (locker) { if (!_lockDics.TryGetValue(key, out entry)) { entry = new MonitorStrEntry(); _lockDics[key] = entry; } entry.Increment(); } var acquired = Monitor.TryEnter(entry, timeoutMillis); if (!acquired) entry.Decrement(); return acquired; } public static void Exit(string key) { var entry = _lockDics[key]; Monitor.Exit(entry); if (entry.Decrement() == 0) { var locker = _lockers[GetIndex(key)]; lock (locker) { if (entry.CanRemove()) { Console.WriteLine(key + "remove"); _lockDics.TryRemove(key, out var v); } } } } class MonitorStrEntry { private int _lockCount; public int Increment() { Interlocked.Increment(ref _lockCount); return _lockCount; } public int Decrement() { Interlocked.Decrement(ref _lockCount); return _lockCount; } public bool CanRemove() { return _lockCount == 0; } } }
這個程式碼的原理就是利用陣列物件鎖和傳入的string key進行對不同key之間的粒度的區分,因為不同的key之間的hashcode不一致所以對取到的鎖物件locker也不一樣,達到降低鎖併發的級別,字典儲存的entry內部維護一個鎖的加鎖次數達,利用cas保證併發多執行緒安全且高效。
如何使用
var key = "testKey"; var timeoutMillis = 3000; var acquired = false; try { acquired = MonitorStr.TryEnter(key, timeoutMillis); if (acquired) { //Do Something } else { throw new XXXTimeOutException(); } } finally { MonitorStr.Exit(key); }
哪個場景下可以使用呢?
//使用場景,諸如多執行緒查資料庫環境的情況下 var userId = "xxxx"; var user = Cache.Query(userId); if (user == null) { var acquired = false; try { acquired = MonitorStr.TryEnter(key, timeoutMillis); if (acquired) { //Do Something user = Cache.Query(userId); if (user == null) { user = DB.Query(userId); } } else { throw new XXXTimeOutException(); } } finally { MonitorStr.Exit(key); } }
謝謝