6

私は、広範な C API 相互運用を必要とするシステムに取り組んでいます。相互運用の一部では、操作の前後に問題のシステムの初期化とシャットダウンが必要です。どちらかを怠ると、システムが不安定になります。次のように、コアの使い捨て環境クラスに参照カウントを実装するだけで、これを実現しました。

public FooEnvironment()
{
  lock(EnvironmentLock)
  {
    if(_initCount == 0)
    {
      Init();  // global startup
    }
    _initCount++;
  }
}

private void Dispose(bool disposing)
{
  if(_disposed)
    return;

  if(disposing)
  {
    lock(EnvironmentLock)
    {
      _initCount--;
      if(_initCount == 0)
      {
        Term(); // global termination
      }
    }
  }
}

これはうまく機能し、目標を達成しました。ただし、相互運用操作はブロックを使用する FooEnvironment にネストする必要があるため、常にロックしており、プロファイリングでは、このロックが実行時に行われる作業の 50% 近くを占めていることが示唆されています。これは基本的な概念であり、.NET または CLR で対処する必要があるように思えます。参照カウントを行うより良い方法はありますか?

4

7 に答える 7

8

これは、一見しただけで予想されるよりもトリッキーな作業です。Interlocked.Increment があなたの仕事に十分であるとは思いません。むしろ、CAS (Compare-And-Swap) を使用して何らかの魔法を実行する必要があると思います。

これをほとんど正しいものにするのは非常に簡単ですが、プログラムがハイゼンバグでクラッシュした場合、ほとんど正しいことは依然として完全に間違っていることに注意してください。

この道を進む前に、本格的な調査を行うことを強くお勧めします。「ロックフリー参照カウント」を検索すると、いくつかの良い出発点が一番上に表示されます。 この Dr. Dobbs の記事は役に立ち、この SO の質問が関連している可能性があります。

何よりも、ロックフリープログラミングは難しいことを覚えておいてください。これが専門でない場合は、一歩下がって、参照カウントの粒度に関する期待を調整することを検討してください。あなたが専門家ではない場合、信頼できるロックフリーメカニズムを作成するよりも、基本的なrefcountポリシーを再考する方がはるかに安価です。特に、ロックフリー手法が実際に高速になることをまだ知らない場合.

于 2012-04-09T15:16:26.127 に答える
1

ハロルドのコメントが指摘しているように、答えはInterlocked次のとおりです。

public FooEnvironment() {
  if (Interlocked.Increment(ref _initCount) == 1) {
    Init();  // global startup
  }
}

private void Dispose(bool disposing) {
  if(_disposed)
    return;

  if (disposing) {
    if (0 == Interlocked.Decrement(ref _initCount)) {
      Term(); // global termination
    }
  }
}

両方とも新しいカウントIncrementDecrement返します(この種の使用法のためだけに)、したがって異なるチェック。

ただし、注意:他に同時実行保護が必要な場合、これは機能しません。Interlocked操作自体は安全ですが、他には何もありません(Interlocked呼び出しの相対的な順序が異なるスレッドを含む)。上記のコードInit()では、別のスレッドがコンストラクターを完了した後も実行できます。

于 2012-04-09T13:55:30.750 に答える
0

これにより、Interlocked.Increment/Decrementを安全に使用できるようになると思います。

:これは単純化されすぎています。Init()が例外をスローすると、以下のコードがデッドロックにつながる可能性があります。カウントがゼロになり、initがリセットされ、コンストラクターが再度呼び出されると、競合状態も発生します。Disposeプログラムの流れがわからないので、dispose呼び出しを数回行った後に再度初期化する可能性がある場合は、InterlockedIncrementではなくSpinLockのような安価なロックを使用する方がよい場合があります。

static ManualResetEvent _inited = new ManualResetEvent(false);
public FooEnvironment()
{
    if(Interlocked.Increment(ref _initCount) == 1)
    {
        Init();  // global startup
        _inited.Set();
    }

    _inited.WaitOne();
}

private void Dispose(bool disposing)
{
    if(_disposed)
        return;

    if(disposing)
    {
        if(Interlocked.Decrement(ref _initCount) == 0)
        {
            _inited.Reset();
            Term(); // global termination
        }
    }
}

編集:
これについてさらに考えると、アプリケーションの再設計を検討することをお勧めします。このクラスの代わりに、InitとTermを管理するために、アプリケーションの起動時にInitを1回呼び出し、アプリがダウンしたときにTermを呼び出すだけです。ロックの必要性を完全に取り除きます。ロックが実行時間の50%として表示されている場合は、常にInitを呼び出したいと思われるので、呼び出すだけですぐに使用できます。

于 2012-04-09T13:55:54.893 に答える
0

使用Threading.Interlocked.Incrementは、ロックを取得し、インクリメントを実行し、ロックを解放するよりも少し速くなりますが、それほど速くはありません。マルチコアシステムでのいずれかの操作のコストのかかる部分は、コア間のメモリキャッシュの同期を強制することです。の主な利点Interlocked.Incrementは速度ではなく、制限された時間内に完了するという事実です。対照的に、ロックを取得し、インクリメントを実行し、ロックを解放しようとすると、カウンターを保護する以外の目的でロックが使用されたとしても、他のスレッドがあれば永遠に待たなければならないリスクがあります。ロックを取得してから、ウェイレイドします。

Concurrent使用している.netのバージョンについては言及していませんが、役立つ可能性のあるクラスがいくつかあります。物を割り当てたり解放したりするパターンによっては、少しトリッキーに見えるかもしれませんが、うまく機能する可能性のあるクラスは、ConcurrentBagクラス。キューやスタックに似ていますが、特定の順序で処理が行われる保証はありません。リソースラッパーに、それがまだ良好かどうかを示すフラグを含め、リソース自体にラッパーへの参照を含めます。リソースユーザーが作成されたら、ラッパーオブジェクトをバッグに入れます。リソースユーザーが不要になったら、「無効」フラグを設定します。「有効」フラグが設定されているラッパーオブジェクトがバッグ内に少なくとも1つあるか、リソース自体が有効なラッパーへの参照を保持している限り、リソースは存続している必要があります。アイテムが削除されたときにリソースが有効なラッパーを保持していないように見える場合は、ロックを取得し、リソースがまだ有効なラッパーを保持していない場合は、有効なラッパーが見つかるまでラッパーをバッグから引き出します。次に、そのリソースをリソースとともに保存します(または、リソースが見つからない場合は、リソースを破棄します)。アイテムが削除されたときにリソースが有効なラッパーを保持しているが、バッグが無効なアイテムを過剰に保持しているように見える場合は、ロックを取得し、バッグの内容を配列にコピーして、有効なアイテムをバッグに戻します。スローされたアイテムの数を数えておくと、次のパージをいつ実行するかを判断できます。

このアプローチは、ロックやを使用するよりも複雑に見えるThreading.Interlocked.Increment場合があり、心配する必要のあるコーナーケースがたくさんありますが、ConcurrentBagリソースの競合を減らすように設計されているため、パフォーマンスが向上する可能性があります。プロセッサ1が実行する場合Interlocked.Incrementある場所で、プロセッサ2がそうすると、プロセッサ2はプロセッサ1にその場所をキャッシュからフラッシュするように指示し、プロセッサ1がそうするまで待って、他のすべてのプロセッサにその場所の制御が必要であることを通知し、その場所をキャッシュに入れ、最後にそれをインクリメントすることに取り掛かります。それがすべて起こった後、プロセッサ1が場所を再度インクリメントする必要がある場合は、同じ一般的な手順のシーケンスが必要になります。これはすべて非常に遅いです。対照的に、ConcurrentBagクラスは、複数のプロセッサがキャッシュの衝突なしにリストに物を追加できるように設計されています。物事が追加されてから削除されるまでの間に、一貫性のあるデータ構造にコピーする必要がありますが、そのような操作は、優れたキャッシュパフォーマンスが得られるようにバッチで実行できます。

を使用して上記のようなアプローチを試したことがないConcurrentBagため、実際にどのようなパフォーマンスが得られるかはわかりませんが、使用パターンによっては、参照カウントよりも優れたパフォーマンスが得られる場合があります。

于 2012-04-12T01:05:34.580 に答える
0

おそらく、クラスで一般的な静的変数を使用します。静的は1つだけであり、特定のオブジェクトに固有のものではありません。

于 2012-04-09T13:49:59.547 に答える
0

インターロック クラス アプローチはロック ステートメントよりも少し高速に動作しますが、インターロック命令はメモリ キャッシュ レイヤーをバイパスする必要があるため、マルチコア マシンでは速度の利点はそれほど大きくない可能性があります。

コードが使用されていないときやプログラムが終了するときに Term() 関数を呼び出すことはどれほど重要ですか?

多くの場合、他の API をラップするクラスの静的コンストラクターに Init() の呼び出しを 1 回入れるだけでよく、Term() の呼び出しについてあまり心配する必要はありません。例えば:

static FooEnvironment() { 
    Init();  // global startup 
}

CLR は、外側のクラスの他のメンバー関数の前に、静的コンストラクターが 1 回呼び出されることを保証します。

一部の (すべてではない) アプリケーション シャットダウン シナリオの通知をフックすることも可能で、クリーン シャットダウンで Term() を呼び出すことができます。この記事を参照してください。http://www.codeproject.com/Articles/16164/Managed-Application-Shutdown

于 2012-04-23T17:08:07.860 に答える
0

次のコードを使用すると、ロックをほとんどなくすことができます。それは間違いなく競合を減らし、これがあなたの主な問題である場合、それはあなたが必要とする解決策になるでしょう.

また、デストラクタ/ファイナライザから Dispose を呼び出すことをお勧めします (念のため)。Dispose メソッドを変更しましdisposingた。引数に関係なく、管理されていないリソースを解放する必要があります。オブジェクトを適切に破棄する方法の詳細については、これを確認してください。

これがお役に立てば幸いです。

public class FooEnvironment
{
    private static int _initCount;
    private static bool _initialized;
    private static object _environmentLock = new object();

    private bool _disposed;

    public FooEnvironment()
    {
        Interlocked.Increment(ref _initCount);

        if (_initCount > 0 && !_initialized)
        {
            lock (_environmentLock)
            {
                if (_initCount > 0 && !_initialized)
                {
                    Init(); // global startup
                    _initialized = true;
                }
            }
        }
    }

    private void Dispose(bool disposing)
    {
        if (_disposed)
            return;

        if (disposing)
        {
            // Dispose managed resources here
        }

        Interlocked.Decrement(ref _initCount);

        if (_initCount <= 0 && _initialized)
        {
            lock (_environmentLock)
            {
                if (_initCount <= 0 && _initialized)
                {
                    Term(); // global termination
                    _initialized = false;
                }
            }
        }

        _disposed = true;
    }

    ~FooEnvironment()
    {
        Dispose(false);
    }
}
于 2012-04-10T09:26:05.613 に答える