开发者

Why have I not seen any implementations of IDisposable implementing concurrency?

开发者 https://www.devze.com 2023-02-16 13:52 出处:网络
When I look through 开发者_StackOverflow社区sample implementations of IDisposable, I have not found any that are threadsafe. Why is IDisposable not implemented for thread safety? (Instead callers have

When I look through 开发者_StackOverflow社区sample implementations of IDisposable, I have not found any that are threadsafe. Why is IDisposable not implemented for thread safety? (Instead callers have a responsibility to make sure only a single thread calls Dispose()).


You should not dispose of an object until you have finished with it. If there are other threads referencing the object and there is a chance they might want to call its methods, you should not be disposing of it.

Therefore it is not neccessary for Dispose to be thread safe.


The only real benefit to a thread-safe Dispose pattern is that you could be guaranteed to get an ObjectDisposedException rather than potentially unpredictable behavior in the event of cross-thread misuse. Note that this means the pattern requires more than thread-safe Dispose; it requires that all methods which rely on the class not being Disposed interlock properly with the Disposal mechanism.

It's possible to do this, but it's a lot of effort to cover a boundary case that only occurs if there is a usage error (i.e. a bug).


Brian Lambert wrote a blog post titled A simple and totally thread-safe implementation of IDisposable.
It contains the following implementation:

using System;
using System.Threading;

/// <summary>
/// DisposableBase class. Represents an implementation of the IDisposable interface.
/// </summary>
public abstract class DisposableBase : IDisposable
{
    /// <summary>
    /// A value which indicates the disposable state. 0 indicates undisposed, 1 indicates disposing
    /// or disposed.
    /// </summary>
    private int disposableState;

    /// <summary>
    /// Finalizes an instance of the DisposableBase class.
    /// </summary>
    ~DisposableBase()
    {
        // The destructor has been called as a result of finalization, indicating that the object
        // was not disposed of using the Dispose() method. In this case, call the DisposeResources
        // method with the disposeManagedResources flag set to false, indicating that derived classes
        // may only release unmanaged resources.
        this.DisposeResources(false);
    }

    /// <summary>
    /// Gets a value indicating whether the object is undisposed.
    /// </summary>
    public bool IsUndisposed
    {
        get
        {
            return Thread.VolatileRead(ref this.disposableState) == 0;
        }
    }

    #region IDisposable Members

    /// <summary>
    /// Performs application-defined tasks associated with disposing of resources.
    /// </summary>
    public void Dispose()
    {
        // Attempt to move the disposable state from 0 to 1. If successful, we can be assured that
        // this thread is the first thread to do so, and can safely dispose of the object.
        if (Interlocked.CompareExchange(ref this.disposableState, 1, 0) == 0)
        {
            // Call the DisposeResources method with the disposeManagedResources flag set to true, indicating
            // that derived classes may release unmanaged resources and dispose of managed resources.
            this.DisposeResources(true);

            // Suppress finalization of this object (remove it from the finalization queue and
            // prevent the destructor from being called).
            GC.SuppressFinalize(this);
        }
    }

    #endregion IDisposable Members

    /// <summary>
    /// Dispose resources. Override this method in derived classes. Unmanaged resources should always be released
    /// when this method is called. Managed resources may only be disposed of if disposeManagedResources is true.
    /// </summary>
    /// <param name="disposeManagedResources">A value which indicates whether managed resources may be disposed of.</param>
    protected abstract void DisposeResources(bool disposeManagedResources);
}

However the absolute and complete totality is disputed a bit in the comments, both on the blog and here.


I'm not sure why Microsoft doesn't use an interlocked Disposing flag in the non-virtual dispose method (with the intention that a finalizer--if any--should use the same flag). Situations where multiple threads might attempt to dispose an object are rare, but it's not forbidden. It could occur, for example, with objects that are supposed to perform some asynchronous task and clean up after themselves, but which can be killed off early if necessary. Object disposal should not occur often enough for an Interlocked.Exchange to have any meaningful performance cost.

On the other hand, it's important to note that while protecting Dispose against multiple invocations is IMHO a wise policy, it is not sufficient to make Dispose really be thread-safe. It is also necessary to ensure that calling Dispose on an object which is in use will leave things in a good state. Sometimes the best pattern for this is to have dispose set a "KillMeNow" flag, and then in a block guarded by Monitor.TryEnter, Dispose the object. Every routine (other than Dispose) which uses the object must acquire the lock during operation, but test both before acquiring and after releasing the lock to see if KillMeNow is set; if so, do a Monitor.TryEnter and perform the dispose logic.

A bigger problem with making a thread-safe IDisposable is the fact that Microsoft does not specify that an event's RemoveHandler method must be thread-safe without risk of deadlock. It is frequently necessary for a IDisposable.Dispose to remove event handlers; without a guaranteed-thread-safe way of doing that, it's almost impossible to write a thread-safe Dispose.


Because they are wrong and lazy, often dismissed as "edge cases" or "not worth the overhead" of being correct.

Use a reader/writer lock to synchronize a disposable class. The thing you're protecting from concurrent writes is the "disposed" state. All methods simply acquire a read lock to use the class, which guarantees they're not disposed mid-call. Because multiple readers are supported, methods do not block each other. The "dispose" method should acquire the writer lock to perform the cleanup, guaranteeing it doesn't run concurrently with any other method, or itself.

This provides complete thread-safety for the dispose method and all other methods in the class. Disposal logic will not run while normal logic runs (and vice versa), and will also not run concurrently with itself.

The reader-writer lock you use should support async, so you can use async code. As a bonus, async locks usually do not support re-entrantrancy. This is good, because you would not want one method calling another anyway, since a 'writer' lock could happen between the two read lock acquisition calls and prevent the second read lock from being taken out indefinitely (deadlock).

If you're willing to go with the "use the class correctly and don't dispose until you're sure you're done with it" line of thought, then this is all just extra overhead. However, I believe it's the only truly correct, full thread-safe approach.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号