8

(質問の改訂):これまでのところ、すべての回答には、再帰などを通じて、ロック領域に直線的に再入る単一のスレッドが含まれています。再帰などでは、単一のスレッドがロックに2回入るステップを追跡できます。しかし、どういうわけか、単一のスレッド(おそらく、タイマーイベントまたは非同期イベントの結果として、またはスレッドがスリープ状態になり、コードの他のチャンクで個別に起動/再利用された結果として、ThreadPoolから)が何らかの形で生成される可能性はありますか? 2つの異なる場所が互いに独立しているため、開発者が自分のコードを読み取るだけでロックの再入力の問題が発生することを予期していなかった場合に、ロックの再入力の問題が発生しますか?

ThreadPoolクラスの備考(ここをクリック)では、備考は、スリープ状態のスレッドを使用していないとき、またはスリープ状態によって無駄になっているときに再利用する必要があることを示唆しているようです。

しかし、Monitor.Enterリファレンスページ(ここをクリック)では、「同じスレッドがブロックせずにEnterを複数回呼び出すことは合法です」と述べています。 だから私は避けるべき何かがあるに違いないと思います。それは何ですか?1つのスレッドが同じロック領域に2回入る可能性さえありますか?

残念ながら長い時間がかかるロック領域があるとします。これは、たとえば、ページアウトされたメモリ(またはその他)にアクセスする場合に現実的です。ロックされた領域のスレッドがスリープ状態になる可能性があります。同じスレッドがより多くのコードを実行できるようになり、誤って同じロック領域にステップインする可能性がありますか?以下は、私のテストでは、同じスレッドの複数のインスタンスを同じロック領域で実行することはできません。

では、どのようにして問題を引き起こすのでしょうか。避けるために注意する必要があるのは正確には何ですか?

class myClass
{
    private object myLockObject;
    public myClass()
    {
        this.myLockObject = new object();
        int[] myIntArray = new int[100];               // Just create a bunch of things so I may easily launch a bunch of Parallel things
        Array.Clear(myIntArray, 0, myIntArray.Length); // Just create a bunch of things so I may easily launch a bunch of Parallel things
        Parallel.ForEach<int>(myIntArray, i => MyParallelMethod());
    }
    private void MyParallelMethod()
    {
        lock (this.myLockObject)
        {
            Console.Error.WriteLine("ThreadId " + Thread.CurrentThread.ManagedThreadId.ToString() + " starting...");
            Thread.Sleep(100);
            Console.Error.WriteLine("ThreadId " + Thread.CurrentThread.ManagedThreadId.ToString() + " finished.");
        }
    }
}
4

6 に答える 6

12

アクションを含むキューがあるとします。

public static Queue<Action> q = whatever;

キューが正常にデキューされたかどうかを示す bool を返すメソッドQueue<T>があるとします。Dequeue

そして、ループがあるとします:

static void Main()
{
    q.Add(M);
    q.Add(M);
    Action action;
    while(q.Dequeue(out action)) 
      action();
}
static object lockObject = new object();
static void M()
{
    Action action;
    lock(lockObject) 
    { 
        if (q.Dequeue(out action))
            action();
    }
}

明らかに、メイン スレッドは M のロックに 2 回入ります。このコードは再入可能です。つまり、間接再帰を介してそれ自体に入ります。

このコードは信じられないように見えますか? そうすべきではありません。これが Windows のしくみです。すべてのウィンドウにはメッセージ キューがあり、メッセージ キューが「ポンピング」されると、それらのメッセージに対応するメソッドが呼び出されます。ボタンをクリックすると、メッセージがメッセージ キューに入ります。キューがポンピングされると、そのメッセージに対応するクリック ハンドラーが呼び出されます。

したがって、ロックにメッセージ ループをポンピングするメソッドの呼び出しが含まれる Windows プログラムを作成することは非常に一般的であり、非常に危険です。最初にメッセージを処理した結果としてそのロックに入った場合、メッセージがキューに 2 回ある場合、コードは間接的に自分自身に入り、あらゆる種類の狂気を引き起こす可能性があります。

これを排除する方法は、(1) ロック内では少し複雑なことでも何もしないこと、および (2) メッセージを処理するときは、メッセージが処理されるまでハンドラーを無効にすることです。

于 2012-12-21T06:45:28.343 に答える
5

次のような構造であれば、再入場が可能です。

Object lockObject = new Object(); 

void Foo(bool recurse) 
{
  lock(lockObject)
   { 
       Console.WriteLine("In Lock"); 
       if (recurse)  { foo(false); }
   }
}

これは非常に単純な例ですが、相互依存または再帰的な動作を行う多くのシナリオで可能です。

例えば:

  • ComponentA.Add(): 共通の「ComponentA」オブジェクトをロックし、新しい項目を ComponentB に追加します。
  • ComponentB.OnNewItem(): 新しいアイテムは、リスト内の各アイテムのデータ検証をトリガーします。
  • ComponentA.ValidateItem(): アイテムを検証するために共通の「ComponentA」オブジェクトをロックします。

独自のコードでデッドロックが発生しないようにするには、同じロックでの同じスレッドの再エントリが必要です。

于 2012-12-21T03:12:24.747 に答える
4

私見、ロックの再入力は避けるために注意する必要があるものではありません(これをロックする多くの人々のメンタルモデルを考えると、これはせいぜい危険です。以下の編集を参照してください)。ドキュメントのポイントは、スレッドがを使用して自分自身をブロックできないことを説明することMonitor.Enterです。これは、すべての同期メカニズム、フレームワーク、および言語に常に当てはまるとは限りません。再入可能でない同期があるものもあります。その場合、スレッドがそれ自体をブロックしないように注意する必要があります。注意する必要があるのは、常にMonitor.ExitすべてのMonitor.Enter呼び出しを呼び出すことです。lockキーワードはこれを自動的に行います。

再入場の簡単な例:

private object locker = new object();

public void Method()
{
  lock(locker)
  {
    lock(locker) { Console.WriteLine("Re-entered the lock."); }
  }
}

スレッドは同じオブジェクトのロックに2回入ったため、2回解放する必要があります。通常、それはそれほど明白ではなく、同じオブジェクト上で同期するさまざまなメソッドが相互に呼び出します。重要なのは、スレッドがそれ自体をブロックすることを心配する必要がないということです。

つまり、通常、ロックを保持するために必要な時間を最小限に抑えるようにする必要があります。ロックの取得は、聞こえるものとは異なり、計算コストが高くありません(数ナノ秒のオーダーです)。ロックの競合は高額です。

編集

詳細については、以下のエリックのコメントをお読みください。ただし、要約するとlock、解釈を見ると、「このコードブロックのすべてのアクティブ化は単一のスレッドに関連付けられている」ということであり、一般的に解釈されているように「」ではないということです。このコードブロックのすべてのアクティブ化は、単一のアトミックユニットとして実行されます。

例えば:

public static void Main()
{
  Method();
}

private static int i = 0;
private static object locker = new object();
public static void Method()
{
  lock(locker)
  {
    int j = ++i;

    if (i < 2)
    {
      Method();
    }

    if (i != j)
    {
      throw new Exception("Boom!");
    }
  }
}

明らかに、このプログラムは爆発します。がなくてlockも、同じ結果になります。危険なのは、初期化と評価のlock間に状態を変更することはできないという誤った安心感につながることです。問題は、あなたが(おそらく意図せずに)自分自身に再発し、それを止められないということです。エリックが彼の答えで指摘しているように、ある日誰かが同時にあまりにも多くのアクションをキューに入れるまで、あなたは問題に気付かないかもしれません。jifMethodlock

于 2012-12-21T05:53:33.430 に答える
4

ロック ブロックに再帰できるより巧妙な方法の 1 つは、GUI フレームワークです。たとえば、単一の UI スレッド (Form クラス) でコードを非同期的に呼び出すことができます。

private object locker = new Object();
public void Method(int a)
{
    lock (locker)
    {
        this.BeginInvoke((MethodInvoker) (() => Method(a)));
    }
}

もちろん、これも無限ループに入ります。無限ループが発生しない時点で再帰したい条件がある可能性があります。

を使用するlockことは、スレッドをスリープ/起動するための良い方法ではありません。Task Parallel Library (TPL) などの既存のフレームワークを使用して抽象タスク (「参考文献」を参照Task) を作成し、基礎となるフレームワークが必要に応じて新しいスレッドの作成とスリープを処理します。

于 2012-12-21T03:24:22.330 に答える
1

ThreadPoolスレッドは、スリープ状態になったという理由だけで他の場所で再利用することはできません。再利用する前に終了する必要があります。ロック領域で長時間かかっているスレッドは、他の独立した制御ポイントでそれ以上のコードを実行する資格がなくなります。ロックの再入力を体験する唯一の方法は、ロックを再入力するロック内のメソッドまたはデリゲートを再帰または実行することです。

于 2013-01-01T13:09:31.617 に答える
0

再帰以外のことを考えてみましょう。
一部のビジネス ロジックでは、同期の動作を制御したいと考えています。これらのパターンの 1 つは、どこかで呼び出し、後で別の場所でMonitor.Enter呼び出したいというものです。Monitor.Exitこれについてのアイデアを得るためのコードは次のとおりです。

public partial class Infinity: IEnumerable<int> {
    IEnumerator IEnumerable.GetEnumerator() {
        return this.GetEnumerator();
    }

    public IEnumerator<int> GetEnumerator() {
        for(; ; )
            yield return ~0;
    }

    public static readonly Infinity Enumerable=new Infinity();
}

public partial class YourClass {
    void ReleaseLock() {
        for(; lockCount-->0; Monitor.Exit(yourLockObject))
            ;
    }

    void GetLocked() {
        Monitor.Enter(yourLockObject);
        ++lockCount;
    }

    void YourParallelMethod(int x) {
        GetLocked();
        Debug.Print("lockCount={0}", lockCount);
    }

    public static void PeformTest() {
        new Thread(
            () => {
                var threadCurrent=Thread.CurrentThread;
                Debug.Print("ThreadId {0} starting...", threadCurrent.ManagedThreadId);

                var intanceOfYourClass=new YourClass();

                // Parallel.ForEach(Infinity.Enumerable, intanceOfYourClass.YourParallelMethod);
                foreach(var i in Enumerable.Range(0, 123))
                    intanceOfYourClass.YourParallelMethod(i);

                intanceOfYourClass.ReleaseLock();

                Monitor.Exit(intanceOfYourClass.yourLockObject); // here SynchronizationLockException thrown
                Debug.Print("ThreadId {0} finished. ", threadCurrent.ManagedThreadId);
            }
            ).Start();
    }

    object yourLockObject=new object();
    int lockCount;
}

を呼び出しYourClass.PeformTest()て lockCount が 1 より大きい場合は、再入力したことになります。必ずしも同時である必要はありません
再入可能性が安全でない場合は、foreach ループでスタックします。がスローされる
コード ブロックでは、入力された回数を超えて呼び出そうとしているためです。このキーワードを使用しようとしている場合、再帰呼び出しの直接的または間接的な場合を除いて、この状況に遭遇することはないでしょう。それがキーワードが提供された理由だと思います。不用意に が省略されるのを防ぎます。 の呼びかけに言及しました。興味がある場合は、楽しくテストできます。Monitor.Exit(intanceOfYourClass.yourLockObject)SynchronizationLockExceptionExitlocklockMonitor.Exit
Parallel.ForEach

コードをテストすること.Net Framework 4.0は最低限の要件であり、次の追加の名前空間も必要です。

using System.Threading.Tasks;
using System.Diagnostics;
using System.Threading;
using System.Collections;

楽しむ。

于 2012-12-31T18:59:24.093 に答える