4

1人の王とn人の手先が彼に提出されている状況を想像してみてください。王が「One!」と言うと、ミニオンの1人が「Two!」と言いますが、そのうちの1人だけです。つまり、最速のミニオンだけが話し、他のミニオンは王の別の呼び出しを待たなければなりません。

これは私の試みです:

using System;
using System.Threading;

class Program {
    static bool leaderGO = false;

    void Leader() {
        do {
            lock(this) {
                //Console.WriteLine("? {0}", leaderGO);

                if (leaderGO) Monitor.Wait(this);

                Console.WriteLine("> One!");
                Thread.Sleep(200);
                leaderGO = true;

                Monitor.Pulse(this);
            }
        } while(true);
    }

    void Follower (char chant) {
        do {
            lock(this) {
                //Console.WriteLine("! {0}", leaderGO);

                if (!leaderGO) Monitor.Wait(this);

                Console.WriteLine("{0} Two!", chant);
                leaderGO = false;

                Monitor.Pulse(this);
            }
        } while(true);
    }

    static void Main() {
        Console.WriteLine("Go!\n");

        Program m = new Program();

        Thread king = new Thread(() => m.Leader());

        Thread minion1 = new Thread(() => m.Follower('#'));
        Thread minion2 = new Thread(() => m.Follower('$'));

        king.Start();

        minion1.Start();
        minion2.Start();

        Console.ReadKey();
        king.Abort();
        minion1.Abort();
        minion2.Abort();
    }
}

期待される出力は次のようになります(#と$は2つの異なるミニオンを表します):

> One!
# Two!
> One!
$ Two!
> One!
$ Two!

...

それらが表示される順序は重要ではなく、ランダムになります。ただし、問題は、このコードをコンパイルすると、代わりに次のように生成されることです。

> One!
# Two!
$ Two!
> One!
# Two!
> One!
$ Two!
# Two!

...

つまり、複数のミニオンが同時に話します。これはさらに多くの手先でかなりの混乱を引き起こすでしょう、そして王はこの種の干渉を許すべきではありません。

考えられる解決策は何でしょうか?


将来の読者のために、これが最終的な実用的なコードです:

using System;
using System.Threading;

class Program { 
    static AutoResetEvent leader = new AutoResetEvent(false);
    static AutoResetEvent follower = new AutoResetEvent(false);

    void Leader() {
        do {
            Console.WriteLine("  One!");
            Thread.Sleep(300);

            follower.Set();     // Leader allows a follower speak
            leader.WaitOne();   // Leader waits for the follower to finish speaking
        } while(true);
    }

    void Follower (char emblem) {
        do {
            follower.WaitOne();     // Follower waits for the leader to allow speaking
            Console.WriteLine("{0} Two!", emblem);
            leader.Set();           // Follower finishes speaking
        } while(true);
    }

    static void Main() {
        Console.WriteLine("Go!\n");

        Program m = new Program();

        Thread king = new Thread(() => m.Leader());

        Thread minion1 = new Thread(() => m.Follower('#'));
        Thread minion2 = new Thread(() => m.Follower('$'));
        Thread minion3 = new Thread(() => m.Follower('&'));

        king.Start();

        minion1.Start();
        minion2.Start();
        minion3.Start();

        Console.ReadKey();
        king.Abort();
        minion1.Abort();
        minion2.Abort();
        minion3.Abort();
    }
}
4

4 に答える 4

4

ロック/モニターの代わりに AutoResetEvent を使用してみてください。一度に 1 つのスレッドだけが通過できる「ゲート」を作成できます。

Follower() スレッドはevent.WaitOne()(オプションでタイムアウト付きで) 呼び出します。Leader() 関数は を呼び出しevent.Set()、待機中のスレッドの 1 つを解放します。

AutoResetEvent (他のタイプの待機ハンドルとは対照的に) は、待機中のスレッドが通過すると、自動的に「ゲートを閉じます」。

http://msdn.microsoft.com/en-us/library/system.threading.autoresetevent.aspx

于 2012-11-20T22:23:58.207 に答える
2

フォロワーをロックダウンしていません。したがって、両方のスレッドは、leaderGo が true であることを認識し、応答します。書き出す前にスレッド自体をロックすると、修正されるはずです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace Threading
{
    class Program
    {
    static bool leaderGO = false;
    static bool followerGo = false;

    void Leader()
    {
        do
        {
            lock (this)
            {
                //Console.WriteLine("? {0}", leaderGO);

                if (leaderGO) Monitor.Wait(this);

                Console.WriteLine("> One!");
                Thread.Sleep(200);
                leaderGO = true;
                followerGo = true;

                Monitor.Pulse(this);
            }
        } while (true);
    }

    void Follower(char chant)
    {
        do
        {
            lock (this)
            {
                //Console.WriteLine("! {0}", leaderGO);

                if (!leaderGO) Monitor.Wait(this);

                if(followerGo)
                {
                    followerGo = false;
                    Console.WriteLine("{0} Two!", chant);
                    leaderGO = false;
                }

                Monitor.Pulse(this);
            }
        } while (true);
    }

    static void Main()
    {
        Console.WriteLine("Go!\n");

        Program m = new Program();

        Thread king = new Thread(() => m.Leader());

        Thread minion1 = new Thread(() => m.Follower('#'));
        Thread minion2 = new Thread(() => m.Follower('$'));

        king.Start();

        minion1.Start();
        minion2.Start();

        Console.ReadKey();
        king.Abort();
        minion1.Abort();
        minion2.Abort();
    }
}

}

于 2012-11-20T22:35:55.050 に答える
2

あなたが経験しているのは競合状態です。ロックされていないリソース (leaderGo) で動作する 2 つの別個のスレッドがあり、クリティカル セクションへのアクセスを制御します (「Two!」を出力します)。

「Two!」を出力する前に、leaderGo にミューテックス ロックを配置する (manman の推奨に従って)。スタートです。また、出力する前に、leaderGo の値がまだ true であることを確認する必要があります。これは、両方のスレッドが最終的にロックを取得しますが、leaderGo が true である間はいずれかのスレッドのみがロックを取得するためです。

何かのようなもの:

lock(leaderGo)
{
     if (leaderGo)
         Console.WriteLine("{0} Two!", chant);
     leaderGo = false;
}

これにより、1 つのフォロワーのみが応答できるようになります (ロックが必要なため)。どのスレッドがロックを取得するか、特定のスレッドがロックを取得する頻度などは保証されません。ただし、各パスですべてのスレッドがロックを取得します。重要なのは、誰が最初にロックしたかだけです。

于 2012-11-20T22:40:40.383 に答える
1

いくつかのヒント:

  • 絶対に使用しないlock(this)でください。オブジェクトを内部からロックすることにより、オブジェクトをロック フォーカスとして使用するものはすべて、独自のコードの同期機能を妨害します。
  • 絶対に使用しないThread.Abort()でください。それは悪です。例外を挿入することで実行中のスレッドを強制終了しますが、これは予測できないため、適切にキャッチして処理することが困難または不可能です。代わりに、ブール型プロパティ IsCancelled を持つクラスのインスタンスを渡してみて、!IsCancelledループを続ける条件として使用してください。

コードの実際の問題は、モニターとロックの組み合わせにより、ロックを取得するスレッドによってクリティカルセクション内からロックが解放されることです。そのスレッドは、他の誰かが最初に行く必要があると考えている場合です。3 つのスレッドがあり、それぞれがロックを取得してから解放し、待機してから、ロックを再取得して、待機していた条件が false であるかのように処理を進めます

考えられるシナリオの 1 つ:

  • フォロワー 1 は、フォロワーのクリティカル セクション (lock() ブロック) に入ります。
  • フォロワー 2 はフォロワーのクリティカル セクションに近づき、待機するように指示されます。
  • キングはリーダーの重要なセクションに近づき、待つように言われます。
  • フォロワー 1 は、leaderGO が false であることを確認して待機し、クリティカル セクションのロックを解放します。
  • King は、2 番目に並んでいるにもかかわらず、Follower 2 よりも前に重要なセクションに「競争」します。
  • King は続行し (leaderGo は false であるため、King は Wait() を実行しません)、「One!」を呼び出します。クリティカル セクションの最後でロックを解除する前に、フラグを設定します。
  • フォロワー 2 は、フォロワー 1 よりも先にクリティカル セクションに「競争」し、フラグが設定されているのを確認し、「Two!」を呼び出して続行します。クリティカルセクションを終了します。
  • フォロワー 1 がターンを取得し、クリティカル セクションの途中でロックを再取得します。LeaderGo が false であることはもはや気にしません。それはすでにそのチェックを過ぎています。そのため、続けて「Two!」を呼び出し、フラグを (既に設定されていた値に) 設定して終了します。

設定方法に基づいて、これらのスレッドが「競合」する可能性のある方法は多数あります。

これはもう少しうまくいくかもしれません。それはダブルチェックロックと呼ばれ、誰にでもできるわけではありませんが、あなたが持っているものよりもはるかに優れています:

private static readonly object syncObj = new object();

void Leader() {
    do {
        if(leaderGo) 
        {
           Thread.Sleep(200);
           continue;
        }
        lock(syncObj) {
            //the "double-check"; here it's not necessary because there's 
            //only one King to set leaderGo to true, 
            //but it doesn't hurt anything.
            if(leaderGo) continue;

            //we won't get here unless we have control of 
            //the critical section AND must do something.
            Console.WriteLine("> One!");
            Thread.Sleep(200);
            leaderGO = true;
        }
    } while(true);
}

void Follower (char chant) {
    do {
        if(!leaderGo) 
        {
           Thread.Yield();
           continue;
        }
        lock(syncObj) {
            //this double-check is critical;
            //if we were waiting on the other follower to release
            //the lock, they have already shouted out and we must not do so.
            if (!leaderGO) continue;

            //we only get here if we have
            //control of the lock and should shout out
            Console.WriteLine("{0} Two!", chant);
            leaderGO = false;                                
        }
    } while(true);
}

編集:コメントで述べたように、このモデルは運に依存しませんが、.NET では、パフォーマンスのために、leaderGO の複数のコピーがさまざまなスレッドのキャッシュに存在することを許可し、それらを背後で同期できるため、絶対確実ではありません。シーン。.NET がその場で同期されていない場合、1 つのスレッドによって実行される二重チェックで、フラグの古い「古い」状態が検出され、代わりにフラグが取り出されるべきときに誤って先に進む可能性があります。

これは、次の 2 つの簡単な方法のいずれかで修正できます。

  • LeaderGO の更新の直後、および LeaderGO の読み取りの直前に MemoryBarrier を配置します。メモリ バリア、または他の言語で「メモリ フェンス」と呼ばれる場合、基本的には、すべてのスレッドがメモリ バリアに到達する (または他の方法でブロックされる) まで、メモリ バリアで実行中の各スレッドをブロックし、その前に発生するすべての命令を確実にブロックします。メモリバリアは、実行後の命令の前に実行されています。
  • LeaderGO を として宣言しvolatileます。volatile 変数は .NET で最適化できません。そのコードを実行する任意のスレッドから非効率的ではありますが、アクセス可能なメモリ内の正確に 1 つの場所に存在することが保証されます。したがって、その値への更新は、他のスレッドによってすぐに確認されます。
于 2012-11-20T22:50:20.290 に答える