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

Iva*_*vov 30 .net c# idisposable thread-safety

想象一下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?
    }
}
Run Code Online (Sandbox Code Playgroud)

Dan*_*ant 14

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

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

  • 是的,Dispose-from-multiple线程的最佳解决方案是:**不要这样做**. (13认同)
  • 这里的关键是,当正确实现线程安全时,您将得到一个 ObjectDisposeException,您可以捕获并处理它,而不是在处理时使用非线程安全版本可能导致的非确定性异常。遗憾的是 BCL 实现没有实现处理的线程安全性。 (2认同)

Dan*_*gby 12

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

这有两点需要注意:

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

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

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

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

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

  • 使其变得不稳定不会阻止任何竞争条件.这里有几个. (8认同)

Rob*_*ine 11

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

像这样的东西:

private int _disposeCount;

public void Dispose()
{
    if (Interlocked.Increment(ref _disposeCount) == 1)
    {
        // disposal code here
    }
}
Run Code Online (Sandbox Code Playgroud)

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

然后每个方法都可以非常简单地使用调用此方法作为屏障检查:

private void ThrowIfDisposed()
{
   if (_disposeCount > 0) throw new ObjectDisposedException(GetType().Name);
}
Run Code Online (Sandbox Code Playgroud)

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

如果你只是关于处理支票本身的意思 - 我上面的例子很好.

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

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

private volatile bool _disposed;

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

        // disposal code here
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

  • @Groo - 请参阅编辑.遗憾的是bool没有任何Interlocked重载 - 但我更喜欢这种线程安全方法而不是非安全方法. (3认同)
  • 如果多个线程试图同时处理一个对象(这会很奇怪),这是有道理的。但是它仍然不能保证它不会在您退出 `ThrowIfDisposed` 后立即在不同方法的中间处理。如果您将对象传递给多个线程,恕我直言,这将是更常见的情况。 (3认同)
  • @Groo:同时"Dispose"可能"自然地"发生的一种情况是中止I/O操作.例如,如果一个线程正在对`Socket`执行阻塞读取,而另一个线程决定第一个线程不应再等待(例如,因为用户点击了"取消")第二个线程解除阻塞的正确方法第一个线程是Dispose socket(这将导致阻塞读取停止等待并立即抛出异常).如果第一个线程在完成其操作后要将套接字Dispose,则两个线程可能同时处理. (3认同)