将线程安全性添加到IDisposable对象的正确方法是什么?

时间:2012-01-19 14:36:54

标签: c# .net thread-safety idisposable

想象一下IDisposable接口的实现,它有一些公共方法。

如果在多个线程之间共享该类型的实例,并且其中一个线程可以处置它,那么确保其他线程在处置后不尝试使用该实例的最佳方法是什么?在大多数情况下,在处理对象之后,其方法必须知道它并抛出ObjectDisposedException或者InvalidOperationException或者至少通知调用代码做错事。我是否需要同步每个方法 - 特别是检查是否处理?使用其他公共方法的所有IDisposable实现都需要是线程安全的吗?


以下是一个例子:

public class DummyDisposable : IDisposable
{
    private bool _disposed = false;

    public void Dispose()
    {
        _disposed = true;
        // actual dispose logic
    }

    public void DoSomething()
    {
        // maybe synchronize around the if block?
        if (_disposed)
        {
            throw new ObjectDisposedException("The current instance has been disposed!");
        }

        // DoSomething logic
    }

    public void DoSomethingElse()
    {
         // Same sync logic as in DoSomething() again?
    }
}

6 个答案:

答案 0 :(得分:12)

Dispose的大多数BCL实现都不是线程安全的。这个想法是由Dispose的调用者来确保在Disposed之前没有其他人正在使用该实例。换句话说,它向上推动同步责任。这是有道理的,否则现在所有其他消费者都需要处理对象在使用时处置的边界情况。

也就是说,如果你想要一个线程安全的Disposable类,你可以在每个公共方法(包括Dispose)周围创建一个锁,并在顶部检查_disposed。如果你有长时间运行的方法,你不想持有整个方法的锁定,这可能会变得更加复杂。

答案 1 :(得分:10)

您可以做的最简单的事情是将私有处置变量标记为volatile,并在方法的开头检查它。如果对象已被处理,则可以抛出ObjectDisposedException

有两点需要注意:

  1. 如果方法是事件处理程序,则不应抛出ObjectDisposedException。相反,如果可能的话,你应该优雅地退出方法。原因是存在竞争条件,在您取消订阅后可以提高事件。 (有关详细信息,请参阅Eric Lippert的this article。)

  2. 这不会阻止您在执行某个类方法时处理您的类。因此,如果您的类具有在处置后无法访问的实例成员,则您将需要设置一些锁定行为以确保对这些资源的访问受到控制。

  3. 微软关于IDisposable的指导说你应该检查处理所有方法,但我个人认为没必要。问题实际上是,如果允许在处理类之后执行方法,则会引发异常或导致意外的副作用。如果答案是肯定的,那么你需要做一些工作以确保不会发生这种情况。

    关于所有IDisposable类是否应该是线程安全的:否。一次性类的大多数用例涉及它们只能由单个线程访问。

    话虽这么说,您可能想要研究为什么您需要您的一次性类是线程安全的,因为它增加了许多额外的复杂性。可能有一个替代实现,使您不必担心一次性类中的线程安全问题。

答案 2 :(得分:8)

我倾向于使用整数而不是布尔值作为存储处置状态的字段,因为这样您就可以使用线程安全的Interlocked类来测试是否已经调用了Dispose。

这样的事情:

private volatile int _disposeCount;

public void Dispose()
{
    if (Interlocked.Increment(ref _disposeCount) == 1)
    {
        // disposal code here
    }
}

这确保只调用一次处理代码,无论方法被调用多少次,并且完全是线程安全的。

然后每个方法都可以简单地使用call this方法作为屏障检查:

private void ThrowIfDisposed()
{
   if (_disposeCount > 0) throw new ObjectDisposedException(GetType().Name);
}

关于同步每个方法 - 你是说一个简单的屏障检查不会 - 你想要阻止其他线程可能已经在实例中执行代码。这是一个更复杂的问题。我不知道你的代码在做什么,但考虑一下你是否真的需要它 - 一个简单的屏障检查不会吗?

如果你只关心处理的支票本身 - 我上面的例子很好。

编辑:回答评论“这和挥发性bool标志有什么区别?有一个名为somethingCount的字段并允许它只保留0和1值有点令人困惑”

易失性与确保读或写操作操作是原子的和安全的有关。它不会使分配检查值线程的过程安全。因此,例如,尽管存在不稳定性,但以下内容并非线程安全:

private volatile bool _disposed;

public void Dispose()
{
    if (!_disposed)
    {
        _disposed = true

        // disposal code here
    }
}

这里的问题是,如果两个线程靠近在一起,第一个可以检查_disposed,读取false,输入代码块并在将_disposed设置为true之前切换出来。第二个然后检查_disposed,看到false并进入代码块。

使用Interlocked可确保赋值和后续读取都是单个原子操作。

答案 3 :(得分:4)

我更喜欢在整数类型对象“dispos”或“state”变量上使用整数和Interlocked.ExchangeInterlocked.CompareExchange;如果enumInterlocked.Exchange可以处理此类类型,我会使用Interlocked.CompareExchange,但唉他们不能。{/ p>

IDisposable和终结器的大多数讨论都没有提到的一点是,虽然IDisposable.Dispose()正在进行时对象的终结器不应该运行,但是类没有办法阻止声明其类型的对象死了然后复活了。可以肯定的是,如果外部代码允许这种情况发生,那么显然不能要求对象“正常工作”,但Dispose和finalize方法应该得到足够的保护,以确保它们不会破坏任何其他对象的状态,通常需要对对象状态变量使用锁或Interlocked操作。

答案 4 :(得分:1)

FWIW,您的示例代码与我的同事和我通常处理此问题的方式相匹配。我们通常在类上定义一个私有的CheckDisposed方法:

private volatile bool isDisposed = false; // Set to true by Dispose

private void CheckDisposed()
{
    if (this.isDisposed)
    {
        throw new ObjectDisposedException("This instance has already been disposed.");
    }
}

然后我们将CheckDisposed()方法称为所有公共方法的顶部。

如果认为可能存在线索争用而不是错误,我还会添加一个公共IsDisposed()方法(类似于Control.IsDisposed)。


更新:根据有关使isDisposed易变的价值的评论,请注意,鉴于我如何使用CheckDisposed()方法,“围栏”问题相当简单。它本质上是一个故障排除工具,用于快速捕获代码在已经处置后调用对象上的公共方法的情况。在公共方法的开头调用CheckDisposed()绝不保证该对象不会在该方法中处理。如果我认为这是我班级设计中固有的风险,而不是我没有考虑的错误条件,那么我使用前面提到的IsDisposed方法以及适当的锁定。

答案 5 :(得分:0)

您必须锁定对要配置的资源的每次访问权限。我还添加了我通常使用的Dispose模式。

public class MyThreadSafeClass : IDisposable
{
    private readonly object lockObj = new object();
    private MyRessource myRessource = new MyRessource();

    public void DoSomething()
    {
        Data data;
        lock (lockObj)
        {
            if (myResource == null) throw new ObjectDisposedException("");
            data = myResource.GetData();
        }
        // Do something with data
    }

    public void DoSomethingElse(Data data)
    {
        // Do something with data
        lock (lockObj)
        {
            if (myRessource == null) throw new ObjectDisposedException("");
            myRessource.SetData(data);
        }
    }

    ~MyThreadSafeClass()
    {
        Dispose(false);
    }
    public void Dispose() 
    { 
        Dispose(true); 
        GC.SuppressFinalize(this);
    }
    protected void Dispose(bool disposing) 
    {
        if (disposing)
        {
            lock (lockObj)
            {
                if (myRessource != null)
                {
                    myRessource.Dispose();
                    myRessource = null;
                }
            }
            //managed ressources
        }
        // unmanaged ressources
    }
}