这是一个并发问题:字符串值用于表示抽象资源,并且对于给定的字符串值,仅允许一个线程工作;但是如果它们的字符串值不同,则多个线程可以同时运行。到目前为止,非常简单:private static readonly Dictionary<String,Object> _locks = new Dictionary<String,Object>();public static void DoSomethingMutuallyExclusiveByName(String resourceName) { Object resourceLock; lock( _locks ) { if( !_locks.TryGetValue( resourceName, out resourceLock ) ) { _locks.Add( resourceName, resourceLock = new Object() ); } } lock( resourceLock ) { EnterCriticalSection( resourceName ); }}但这不是次优的:resourceName的域是无界的,并且_locks最终可能包含数千个或更多字符串。因此,在没有更多线程正在使用特定的resourceName值之后,则应从字典中删除其锁定对象。由于下面的这种情况(相关代码位于下面),仅在使用了锁定对象后删除该对象可能是一个错误。请注意,所有三个线程均具有resourceName = "foo"。线程1位于labelC,刚刚从resourceName = "foo"字典中删除了它的_locks,从而删除了resourceLock (#1)。线程2位于labelB,因为没有其他线程锁定在resourceLock (#1)上,所以它不等待lock( resourceLock,而是继续进入EnterCriticalSection。线程3位于labelA,并且由于resourceName = "foo"不在_locks中(因为线程1已将其删除),因此它向_locks添加了一个新实例,并且由于此resourceLock (#2)是新实例,因此lock( resourceLock )位于线程3不会等待线程2,因此,线程2和3都可以在EnterCriticalSection内部且具有相同的resourceName值。码:public static void DoSomethingMutuallyExclusiveByName(String resourceName) {labelA: Object resourceLock; lock( _locks ) { if( !_locks.TryGetValue( resourceName, out resourceLock ) ) { _locks.Add( resourceName, resourceLock = new Object() ); } } try {labelB: lock( resourceLock ) { EnterCriticalSection( resourceName ); } } finally { lock( _locks ) { _locks.Remove( resourceName ); }labelC: }}我最初用自己的小技巧解决了这个问题:class CountedLock { public Int32 Count;}private static readonly Dictionary<String,CountedLock> _locks = new Dictionary<String,CountedLock>();public static void DoSomethingMutuallyExclusiveByName(String resourceName) {labelA: CountedLock resourceLock; lock( _locks ) { if( !_locks.TryGetValue( resourceName, out resourceLock ) ) { _locks.Add( resourceName, resourceLock = new CountedLock() { Count = 1 } ); } else { resourceLock.Count++; // no need for Interlocked.Increment as we're already in a mutex code block } } try {labelB: lock( resourceLock ) { EnterCriticalSection( resourceName ); } } finally { lock( _locks ) {labelD: if( --resourceLock.Count == 0 ) { _locks.Remove( resourceName ); } }labelC: }}这样就解决了问题。假设三个线程与以前位于相同的位置:线程1并未从resourceName中删除​​其resourceLock _locks值,因为线程2也引用了与线程1相同的resourceLock,因此当线程1位于2时,相关计数为labelD。 (然后,当线程1达到1时,计数变为labelC)。线程2继续进入关键部分,因为它到达线程3之前。如果在线程2仍处于关键部分时线程3(位于labelA)进入了TryGetValue,则将看到其resourceName="foo"仍在_locks词典中,因此获得与线程2相同的实例,因此它将在labelB等待线程2完成。这样就行了!但我想您现在正在思考: “因此,您拥有一个带有相关计数的锁……听起来就像您只是重新发明了信号灯。不要重新发明轮子,请使用System.Threading.Sempahore或SemaphoreSlim。确实,所以我更改了代码以使用SemaphoreSlim-一个SemaphoreSlim实例具有一个内部Count值,该值是允许进入的线程数(与当前“在”信号量内部的线程数相反-这与我的CountedLock示例在上一个示例中的工作方式相反):private static readonly Dictionary<String,SemaphoreSlim> _locks = new Dictionary<String,SemaphoreSlim>();public static void DoSomethingMutuallyExclusiveByName(String resourceName) {labelA: SemaphoreSlim resourceLock; lock( _locks ) { if( !_locks.TryGetValue( resourceName, out resourceLock ) { _locks.Add( resourceName, resourceLock = new SemaphoreLock( initialCount: 1, maxCount: 1 ) ); } }labelB: resourceLock.Wait(); // this decrements the semaphore's count try { EnterCriticalSection( resourceName ); } finally { lock( _locks ) { Int32 count = resourceLock.Release(); // this increments the sempahore's count if( count > 0 ) { _locks.Remove( resourceName ); resourceLock.Dispose(); }labelC: } }}...但是发现错误!考虑这种情况:线程1位于labelC处,只是被删除并释放了其resourceLock (#1)实例。线程2位于labelB(在调用Wait之前)。它已获取与线程1相同的SemaphoreSlim resourceLock (#1)实例的引用;但是由于Wait方法是在线程离开lock( _locks )下的labelA之后调用的,这意味着存在一个很小的机会窗口,线程2然后将调用resourceLock (#1).Wait()(忽略可能的),而线程3(当前位于ObjectDisposedException)随后将输入labelA并为同一TryGetValue实例化SemaphoreLock (#2)的新实例,但是由于线程2和线程3具有不同的信号量实例,它们都可能同时进入关键部分。您可能会建议: 当您位于resourceName下的lock( _locks )块中时,您应该找到一种减少信号量的方法...除了labelA类不公开任何SemaphoreSlim方法。您可以调用Decrement使其立即返回,因此我的代码如下所示:[...]labelA: SemaphoreSlim resourceLock; lock( _locks ) { if( !_locks.TryGetValue( resourceName, out resourceLock ) { _locks.Add( resourceName, resourceLock = new SemaphoreLock( initialCount: 1, maxCount: 1 ) ); } resourceLock.Wait( 0 ); }labelB: resourceLock.Wait();[...]...除非这行不通。 .Wait(0)状态的文档(重点是我的): 如果在调用Wait(Int32)时某个线程或任务被阻止,并且由millisecondsTimeout指定的超时间隔到期,则该线程或任务不会输入信号量,并且CurrentCount属性不会减少。...对于该理论而言是如此即使它确实起作用,在同一线程中两次调用Wait(Int32)可能会使计数减少两次,而不是一次。那么,是否有可能由互斥锁保护一个临界区,而该互斥锁以某种方式“知道”何时不再需要它们? (adsbygoogle = window.adsbygoogle || []).push({}); 最佳答案 实际上,我会坚持使用更简单的基于计数的解决方案,而不是SemaphoreSlim,因为您已经实施了它。虽然SemaphoreSlim被称为“ slim”,但它比简单的计数器轻巧。如您所知,使用信号量实际上会使代码的性能稍差一些,并且更加复杂。如果没有别的,如果花更多的时间说服该版本确实有效,那么也许不是更好的版本。因此,也许您正在重新发明轮子,但是SemaphoreSlim是通用信号灯,具有您完全不需要的功能。当SemaphoreSlim已经存在时,甚至Microsoft也通过在BCL中添加Semaphore来重新发明轮子。另一方面,如果您认为争用可能是全局锁的问题,则可以尝试使用无锁方法。最有可能的是,您不会遇到此类问题,但是如果您真的认为在整个代码中都会调用数千次,则可以选择以下内容:private static readonly ConcurrentDictionary<string, CountedLock> _locks = new ConcurrentDictionary<string, CountedLock>();public static void DoSomethingMutuallyExclusiveByName(string resourceName){ CountedLock resourceLock; // we must use a loop to avoid incrementing a stale lock object var spinWait = new SpinWait(); while (true) { resourceLock = _locks.GetOrAdd(resourceName, i => new CountedLock()); resourceLock.Increment(); // check that the instance wasn't removed in the meantime if (resourceLock == _locks.GetOrAdd(resourceName, i => new CountedLock())) break; // otherwise retry resourceLock.Decrement(); spinWait.SpinOnce(); } try { lock (resourceLock) { // EnterCriticalSection(resourceName); } } finally { if (resourceLock.Decrement() <= 0) _locks.TryRemove(resourceName, out resourceLock); }}同时将CountedLock修改为使用Interlocked类:class CountedLock{ Int32 _count; public int Increment() => Interlocked.Increment(ref _count); public int Decrement() => Interlocked.Decrement(ref _count);}无论哪种方式,我都可能将代码重组为通用代码,并(ab)使用IDisposable接口以允许您将调用简单地包装在单个using块中。 (adsbygoogle = window.adsbygoogle || []).push({});
07-24 21:33