【C#】Interlockedを使ったDisposeパターンの実装

以前以下のような記事を書いたので、Interlocked を使って Dispose の同時実行性能を少し強化してみたいと思います。

前に書いた記事 ↓↓↓↓

【C#】Interlockedを使って同時実行数を制限する - PG日誌

確認環境

実装・確認環境は以下の通りです。

  • .NET8
  • VisualStudio2022
  • Windows11

一般的なDispose実装例

まずは一般的な実装の確認です。

フラグを使ってDisposeしたかどうかをこんな感じにチェックすると思います。

// よくあるDisposeの処理
public class DisposeSample : IDisposable
{
    // 破棄したかどうかのフラグ
    // true: 破棄済み / false: まだ
    bool _disposed;

    // IDisposableの実装(Disposeパターン)

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {   
        if (_disposed)
        {
            return; // 破棄済みなら何もしない
        }
        _disposed = true;

        if (disposing)
        {
            // マネージリソースの開放
        }

        // アンマネージリソースの開放
    }

    public void Foo()
    {
        ObjectDisposedException.ThrowIf(_disposed, this);

        // 何かの処理...
        Thread.Sleep(TimeSpan.FromSeconds(1.0));
    }

    public async Task FooAsync(CancellationToken ct = default)
    {
        ObjectDisposedException.ThrowIf(_disposed, this);

        // 何かの処理...
        await Task.Delay(TimeSpan.FromSeconds(1.0), ct);
    }
}

同時実行を強化したDispose

先ほどの例が何が問題かですが

  • Dispose を呼んだ瞬間に Foo や FooAsync などを呼び出すと処理が実行できることがある
  • Dispose をほぼ同時に呼び出すと2回以上実行できてしまう場合がある

この状況が発生する事自体が呼び出し側の設計が悪い気がしますが、どうしても避けられない時に(厄介なマルチスレッド)バグを発生させないためにチェックを厳格化する時用の、穴を潰すための実装例です。

以下、Interlocked を使って Dispose のフラグ管理を強化した版です。

// 破棄とメソッドの実行の同時実行を低減する
public class DisposeSample2 : IDisposable
{
    // 破棄したかどうかのフラグ
    // 1: 破棄済み / 0: まだ
    int _disposed;

    // 破棄済みかどうかをboolで返すプロパティ(ThrowIf向け)
    private bool IsDisposed => Volatile.Read(ref _disposed) == 1;

    // IDisposableの実装(Disposeパターン)

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        // ★ここでアトミック操作なフラグ更新を行う
        if (Interlocked.Exchange(ref _disposed, 1) == 1)
        {
            return; // 破棄済みなら何もしない
        }

        if (disposing)
        {
            // マネージリソースの開放
        }

        // アンマネージリソースの開放
    }

    public void Foo()
    {
        ObjectDisposedException.ThrowIf(IsDisposed, this);

        // 何かの処理...
        Thread.Sleep(TimeSpan.FromSeconds(1.0));
    }

    public async Task FooAsync(CancellationToken ct = default)
    {
        ObjectDisposedException.ThrowIf(IsDisposed, this);

        // 何かの処理...
        await Task.Delay(TimeSpan.FromSeconds(1.0), ct);
    }
}

これで少なくとも Dispose を呼びだした後はチェックが入ってるメソッドでは ObjectDisposedException が期待通り発生するようになります(ちなみにこの実装だと Foo とか FooAsync のメソッド実行中に Dispose が呼ばれることまではフォローしてません)

FooAsync が実行中 → Dispose が呼ばれるのようなケース以外はこれで事前に予防できると思います。

関連

takap-tech.com

takap-tech.com