使用 IDisposable 模式在 C# 中同步对资源的访问的方法

Posted

技术标签:

【中文标题】使用 IDisposable 模式在 C# 中同步对资源的访问的方法【英文标题】:Approach for synchronizing access to a resource in C# using the IDisposable pattern 【发布时间】:2021-06-17 00:19:06 【问题描述】:

我正在考虑一种使用 IDisposable 模式来同步/协调对共享资源的访问的方法。

到目前为止,这是我的代码(易于使用 LinqPad 运行):

#define WITH_CONSOLE_LOG
//better undefine WITH_CONSOLE_LOG when testing long loops

public abstract class SynchronizedAccessBase

    private readonly object syncObj = new();

    private class AccessToken : IDisposable
    
        private SynchronizedAccessBase parent;
        private bool didDispose;

        public AccessToken(SynchronizedAccessBase parent)
        
            this.parent = parent;
        

        protected virtual void Dispose(bool disposing)
        
            if (!this.didDispose)
            
                Monitor.Exit(this.parent.syncObj);
#if WITH_CONSOLE_LOG
                Console.WriteLine("Monitor.Exit by Thread=" + Thread.CurrentThread.ManagedThreadId);
#endif
                if (disposing)
                
                    //nothing specific here
                
                this.didDispose = true;
            
        
        ~AccessToken()
        
            this.Dispose(disposing: false);
        

        void IDisposable.Dispose()
        
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        
    

    public IDisposable WantAccess()
    
        Monitor.Enter(this.syncObj);
#if WITH_CONSOLE_LOG
        Console.WriteLine("Monitor.Enter by Thread=" + Thread.CurrentThread.ManagedThreadId);
#endif
        return new AccessToken(this);
    


public class MyResource : SynchronizedAccessBase

    public int Value;


private MyResource TheResource;

private void MyMethod()

    using var token = TheResource.WantAccess(); //comment out this line to see the unsynced behavior
#if WITH_CONSOLE_LOG    
    Console.WriteLine("Inc'ing Value by Thread=" + Thread.CurrentThread.ManagedThreadId);
#endif
    TheResource.Value++;


void Main()

    this.TheResource = new MyResource();
    
    var inc_tasks = new Task[10];
    for (int i = 0; i < inc_tasks.Length; i++)
        inc_tasks[i] = Task.Run(() =>
        
            for (int loop = 1; loop <= 100; loop++)
                MyMethod();
        );

    Task.WaitAll(inc_tasks);

    Console.WriteLine("End of Main() with Value==" + TheResource.Value);

我想要实现的是在(独占)访问共享资源之前在方法顶部(或中间某处,谁在乎)使用 C#“使用”语句,并具有 IDisposable 机制自动结束独占访问。

在后台,Monitor 类用于此目的。

理想的优势是不需要缩进的 代码块 。只是一个 using... 行,就是这样。请参阅上面示例中的 MyMethod()。

这似乎工作得很好。所有公司的最终结果都符合预期,即使循环很长,如果我从 MyMethod 中删除 using.. 语句也是错误的。

但是,您认为我可以信任此解决方案吗? .Dispose 是否真的,真的总是在离开 MyMethod 时调用,即使出现异常?其他陷阱?

谢谢!

【问题讨论】:

是的,这是一种有效的方法。您可以看到类似使用 here 的示例(Releaser 类)。对于同步锁定,使用结构而不是类作为Releaser 的类型可能会更有效。 我尝试使用 struct 而不是 AccessToken 的类。是的,这明显更快(30-40%) 如果您决定将其设为结构,则应考虑将WantAccess 方法的返回类型从IDisposable 更改为AccessToken(需要将AccessToken 设为公开)。这样该结构就不会被using 语句装箱,从而减少分配和垃圾收集器的工作。 @TheodorZoulias,我也试过了,但我无法衡量性能提升 也许protected virtual~AccessToken() 终结器和GC.SuppressFinalize(this); 调用会减慢速度。你真的需要这些东西吗?您是否预计必须从 AccessToken 类型继承的情况? 【参考方案1】:

如果在您进入 WantAccess 中的监视器之后并且在将返回值分配给 using 块中的变量之前引发异常,您的代码可能会在临界区范围之外持有锁。 lock 由于转换的方式,不可能做同样的事情。

lock 是专门为这个问题而设计的,并且经过非常精细的调整,可以在解决这类问题时完全按照您的意愿去做。您应该为工作使用正确的工具。

【讨论】:

(如果抛出异常则保持锁定)是的,这是一个好地方,而且那种错误是重复和发现的噩梦。你可以通过一些异常处理来解决这个问题。 @Servy 我明白你的意思。但是,我想不出为什么在成功调用 Monitor.Enter() 并且在将 WantAccess 的结果分配给 token 变量之前会发生这种异常的原因。好的,new AccessToken() 上可能会发生内存不足异常。因此,可以在 Monitor.Enter() 调用之前更好地准备该实例。其他想法可能会出什么问题(在现实世界中)? @mikelegg 遗憾的是你不能。你可以处理一些情况,但不是全部。 lock 的作用是在 try/finally 中调用 Monitor.TryEnter,以便最终检查是否已获取锁并释放它,您需要复制它以涵盖它所做的所有情况。没有using 实现能够复制它。 @KarloX 好吧,它只需要一个,你提到了一个,所以这足以成为一个破坏者。最重要的是,您可能会出现堆栈溢出和线程中止异常。最后,您正在访问标准输出,这可能由于多种原因而引发。 @Servy,好的标准输出当然不会变成生产代码。线程中止是一个有效点。仅返回结果并将其分配给声明的局部变量时的堆栈溢出,嗯,我不知道,也许。谢谢【参考方案2】:

我认为这没有任何问题。我之前以类似的方式使用了 using 模式,没有问题。

作为一个简单的例子,考虑使用数据库连接的模式。下面有一个连接池,从池中创建一个新连接(可能正在等待)并将释放释放回池。这和你的一样,都是 1 个项目。

如果资源仅在 1 个进程内共享,我不知道仅使用简单的 lock() 模式是否值得。那会更“传统”。但只有你能回答。

【讨论】:

以上是关于使用 IDisposable 模式在 C# 中同步对资源的访问的方法的主要内容,如果未能解决你的问题,请参考以下文章

C#中的IDisposable模式用法详解

c# 中最简单的 IDisposable 模式是啥? [复制]

C#中IDisposable的用法

在 C# 中实现 IDisposable [重复]

C# IDisposable 正确用法和调用

C#中对IDisposable接口的理解