簡體   English   中英

將線程安全性添加到IDisposable對象的正確方法是什么?

[英]What is the correct way of adding thread-safety to an IDisposable object?

想象一下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?
    }
}

Dispose的大多數BCL實現都不是線程安全的。 這個想法是由Dispose的調用者來確保在Disposed之前沒有其他人正在使用該實例。 換句話說,它向上推動同步責任。 這是有道理的,否則現在所有其他消費者都需要處理對象在使用時處置的邊界情況。

也就是說,如果你想要一個線程安全的Disposable類,你可以在每個公共方法(包括Dispose)周圍創建一個鎖,並在頂部檢查_disposed。 如果你有長時間運行的方法,你不想持有整個方法的鎖,這可能會變得更加復雜。

您可以做的最簡單的事情是將私有處置變量標記為volatile ,並在方法的開頭檢查它。 如果已經處置了對象,則可以拋出ObjectDisposedException

這有兩點需要注意:

  1. 如果方法是事件處理程序,則不應拋出ObjectDisposedException 相反,如果可能的話,你應該優雅地退出方法。 原因是存在競爭條件,在您取消訂閱后可以提高事件。 (有關更多信息,請參閱Eric Lippert 撰寫的這篇文章 。)

  2. 當你正在執行一個類方法時,這並不會阻止你的類處理。 因此,如果您的類具有在處置后無法訪問的實例成員,則您將需要設置一些鎖定行為以確保控制對這些資源的訪問。

微軟關於IDisposable的指導說你應該檢查處理所有方法,但我個人認為沒必要。 問題實際上是,如果允許在處理類之后執行方法,則會引發異常或導致意外的副作用。 如果答案是肯定的,那么您需要做一些工作以確保不會發生這種情況。

關於所有IDisposable類是否應該是線程安全的:否。一次性類的大多數用例涉及它們只能被單個線程訪問。

話雖如此,您可能想要研究為什么您需要您的一次性類是線程安全的,因為它增加了許多額外的復雜性。 可能有一個替代實現,使您不必擔心一次性類中的線程安全問題。

我傾向於使用整數而不是布爾值作為存儲處置狀態的字段,因為這樣您就可以使用線程安全的Interlocked類來測試是否已經調用了Dispose。

像這樣的東西:

private int _disposeCount;

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

這樣可以確保只調用一次處理代碼,無論方法被調用多少次,並且完全是線程安全的。

然后每個方法都可以非常簡單地使用調用此方法作為屏障檢查:

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可確保賦值和后續讀取都是單個原子操作。

我更喜歡在整數類型對象“dispos”或“state”變量上使用整數和Interlocked.ExchangeInterlocked.CompareExchange ; 如果Interlocked.ExchangeInterlocked.CompareExchange可以處理這些類型,我會使用enum ,但唉他們不能。

IDisposable和終結器的大多數討論中沒有提到的一點是,雖然IDisposable.Dispose()正在進行時對象的終結器不應該運行,但是類沒有辦法阻止其類型的對象被聲明為死亡然后復活。 objects' state, which will in turn generally require using either locks or Interlocked operations on object state variables. 可以肯定的是,如果外部代碼允許發生,那么顯然不能要求對象“正常工作”,但Dispose和finalize方法應該受到足夠的保護,以確保它們不會破壞任何對象'state,反過來通常要求對對象狀態變量使用鎖或Interlocked操作。

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 volatile的值的isDisposed ,請注意,鑒於我如何使用CheckDisposed()方法,“fence”問題相當簡單。 它本質上是一個故障排除工具,用於快速捕獲代碼在已經處置后調用對象上的公共方法的情況。 在公共方法的開頭調用CheckDisposed()絕不保證該對象不會在該方法中處理。 如果我認為這是我班級設計中固有的風險,而不是我沒有考慮的錯誤條件,那么我使用前面提到的IsDisposed方法以及適當的鎖定。

您必須鎖定對要配置的資源的每次訪問權限。 我還添加了我通常使用的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
    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM