抽象锁模式,用于选择性地同步实现(“IOC”和 DRY)






3.90/5 (7投票s)
这演示了一个简单的模式,它提供了一个始终可以调用的锁;并且对于非同步实现来说,它可能是一个空操作。
引言
给定一些实现——例如,通过一个接口(像一个集合)——其中“IsSynchronized
”定义了实现类型——该实现必须是同步的或不是——这个简单的模式插入一个“真正的”锁,或者一个空操作。
结果是*单个*实现,具有同步或非同步的行为。
锁被抽象成一个接口;并且在这一点上,它甚至可以被检测:实际的锁实现可以提供内部检查,甚至在单个已实现的实例中随着时间的推移而改变。
给定的例子很简单:该实现需要一个简单的“块”锁定方案;但是,可以根据需要重新定义锁接口,并且您可以实现其他类型的锁定,包括可重入选择、退出并重新进入、尝试进入等等。
Using the Code
该模式的示例用例是,再次,为可以实现为“同步”或“非同步”的接口提供实现。 *单个*实现**总是**插入一个锁;在这个简单的例子中,对于非同步实现来说,这只是一个*空操作*。
例如,这个接口
public interface IBag
{
bool IsSynchronized { get; }
void Invoke();
}
该接口指定是否IsSynchronized
;在这里我们可以使用单个实现来实现两者
public class Bag
{
private readonly ISyncLock syncLock;
/// <summary>
/// Constructor.
/// </summary>
public Bag(bool isSynchronized)
{
syncLock = isSynchronized
? new SyncLock()
: NoOpSyncLock.Instance;
IsSynchronized = isSynchronized;
}
public bool IsSynchronized { get; }
public void Invoke()
{
// Lock the selected implementation:
using (syncLock.Enter()) {
// Act ...
}
}
}
简单的实现*总是*在关键部分调用一个锁,但是所选的锁实际上可能只是一个空操作。
这个简单的“块”同步只是一个例子:锁定要求可能更复杂,但是,锁定实现仍然可以通过单个接口始终插入,并且只需执行一个空操作,或者也可以更改或检测以改变实现。 如果可能,您现在可以将锁定要求**推入**锁定类中,并让您的实现仅遵循该模式。
如示例代码所示,锁定对象甚至可以在可以参与该实现的内部类之间共享。
因此……该实现是 IOC,并且是 DRY。
ISyncLock
实现是这样的
/// <summary>
/// Defines a Monitor that is entered explicitly, and
/// exited when a returned <see cref="IDisposable"/> object is Disposed.
/// </summary>
public interface ISyncLock
{
/// <summary>
/// Enters the Monitor. Dispose the result to exit.
/// </summary>
IDisposable Enter();
}
/// <summary>
/// Implements <see cref="ISyncLock"/> with <see cref="Monitor.Enter(object)"/>
/// and <see cref="Monitor.Exit"/>.
/// </summary>
public sealed class SyncLock
: ISyncLock,
IDisposable
{
private readonly object syncLock;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="syncLock">Optional: if null, <see langword="this"/>
/// is used.</param>
public SyncLock(object syncLock)
=> this.syncLock = syncLock ?? this;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public IDisposable Enter()
{
Monitor.Enter(syncLock);
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Dispose()
=> Monitor.Exit(syncLock);
}
/// <summary>
/// Implements a do-nothing <see cref="ISyncLock"/>.
/// </summary>
public sealed class NoOpSyncLock
: ISyncLock,
IDisposable
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public IDisposable Enter()
=> this;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Dispose() { }
}
NoOpSyncLock
应该非常便宜。
请注意,您**不**锁定 ISyncLock
对象;*并且*您*不直接 Dispose*它。
使用 Enter
方法获取正确的 IDisposable
。
该细节确保了正确的**封装**。
这**也是**为什么 ISyncLock
接口不扩展 IDisposable
。
……除了提供不同的实现之外,您还可以增强或修改该接口。
扩展
请注意,可以检测锁定实现。 就像现在一样,如果您想使用 TryEnter
进入并可选择抛出异常,则可以在一个地方更改它
public IDisposable Enter()
{
if (Monitor.TryEnter(syncLock, myTimeout))
return this;
throw new TimeoutException("Failed to acquire the lock.");
}
您可以自由地使用 TryEnter
方法增强该接口
public IDisposable TryEnter(out bool gotLock)
{
if (Monitor.TryEnter(syncLock, myTimeout)) {
gotLock = true;
return this;
}
gotLock = false;
return NoOpSyncLock.Instance;
}
然后,您的实现**必须**检查 out gotLock
结果;并且不执行任何不安全的操作——No-Op 锁只会始终返回 true。
您可以根据您的需要定义接口;并在您的实现中遵循相同的逻辑。
可以进行更多实现。 using
惯用语*也可以*让您在同一块中安全地退出并重新进入锁——例如,实现可以在进入时使用计数器和/或令牌;并且 Dispose
方法可以安全地每次退出。 也可以实现一个 async
锁……重点是锁*接口*提供了所需的单一模式来包装您需要的实现;并且可以在不需要同步的地方进行交换和存根,或者,*无法*实现……并且如上所述,可以在可以实现它的内部代码之间共享单个锁定实现。
关注点
- 可以检测锁定实现,并全局更改。
- 完整的实现选择器是在一个类中实现的……DRY。
- 该实现现在正在使用控制反转。
- 空操作的运行时成本应该非常便宜。
附加代码
有一个简单的示例附加了一个人为的用例;这说明了这一点。
历史
- 2018年8月30日:初始版本