16

条件変数クラスを構築するための私の探求において、私はそれを行うための簡単な方法に出くわしました、そして私はこれをスタックオーバーフローコミュニティと共有したいと思います。私は1時間の大部分をグーグルで検索していましたが、正しいと感じた良いチュートリアルや.NET風の例を実際に見つけることができませんでした。うまくいけば、これは他の人にも役立つでしょう。

4

6 に答える 6

27

lockとのセマンティクスを理解すれば、実際には非常に簡単ですMonitor

ただし、最初に、オブジェクト参照が必要です。を使用できますが、クラスへの参照を持つ誰もがその参照をロックできるという意味で、それthisを覚えておいてください。これに不安がある場合は、次のように新しいプライベート参照を作成できます。thispublic

readonly object syncPrimitive = new object(); // this is legal

通知を提供できるようにしたいコードのどこかで、次のように実行できます。

void Notify()
{
    lock (syncPrimitive)
    {
        Monitor.Pulse(syncPrimitive);
    }
}

そして、実際の作業を行う場所は、次のような単純なループ構造です。

void RunLoop()
{
    lock (syncPrimitive)
    {
        for (;;)
        {
            // do work here...
            Monitor.Wait(syncPrimitive);
        }
    }
}

外部から見ると、これは非常にデッドロックっぽく見えますが、のロックプロトコルは、Monitorを呼び出すとロックを解放します。実際には、、またはMonitor.Waitを呼び出す前にロックを取得している必要があります。Monitor.PulseMonitor.PulseAllMonitor.Wait

このアプローチには、知っておくべき1つの注意点があります。あなたの通信メソッドを呼び出す前にロックを保持する必要があるので、Monitor実際にはできるだけ短い時間だけロックを保持する必要があります。長時間実行されるバックグラウンドタスクに対してより使いやすいバリエーションは、次のRunLoopようになります。

void RunLoop()
{

    for (;;)
    {
        // do work here...

        lock (syncPrimitive)
        {
            Monitor.Wait(syncPrimitive);
        }
    }
}

しかし、共有リソースを保護するためにロックが使用されなくなったため、問題を少し変更しました。したがって、共有リソースdo work here...にアクセスする必要があるコードを作成する場合は、そのリソースを保護する追加のロックが必要です。

上記のコードを利用して、単純なスレッドセーフなプロデューサーコンシューマーコレクションを作成できます。.NETはすでに優れたConcurrentQueue<T>実装を提供していますが、これはこのように使用することの単純さを示すためだけのMonitorものです。

class BlockingQueue<T>
{
    // We base our queue, on the non-thread safe 
    // .NET 2.0 queue collection
    readonly Queue<T> q = new Queue<T>();

    public void Enqueue(T item)
    {
        lock (q)
        {
            q.Enqueue(item);
            System.Threading.Monitor.Pulse(q);
        }
    }

    public T Dequeue()
    {
        lock (q)
        {
            for (; ; )
            {
                if (q.Count > 0)
                {
                    return q.Dequeue();
                }
                System.Threading.Monitor.Wait(q);
            }
        }
    }
}

ここで重要なのは、.NET Frameworkでも使用できるブロッキングコレクションを作成することではありません(BlockingCollectionを参照)。Monitor重要なのは、.NETのクラスを使用してイベント駆動型メッセージシステムを構築し、条件変数を実装することがいかに簡単であるかを説明することです。これがお役に立てば幸いです。

于 2013-03-27T11:22:52.750 に答える
7

ManualResetEventを使用する

条件変数に似たクラスはManualResetEventですが、メソッド名が少し異なります。

notify_one()C ++ではSet()C#で名前が付けられます。C ++ではC#で
名前が付けられます。wait()WaitOne()

さらに、ManualResetEventReset()は、イベントの状態を非シグナリングに設定するメソッドも提供します。

于 2017-08-29T10:11:52.887 に答える
5

受け入れられた答えは良いものではありません。Dequeue()コードによると、Wait()は各ループで呼び出されるため、不要な待機が発生し、過度のコンテキストスイッチが発生します。正しいパラダイムは次のとおりです。待機条件が満たされたときにwait()が呼び出されます。この場合、待機条件はq.Count()==0です。

モニターを使用する場合に従うべきより良いパターンは次のとおりです。 https://msdn.microsoft.com/en-us/library/windows/desktop/ms682052%28v=vs.85%29.aspx

C#モニターに関する別のコメントは、条件変数を使用しないことです(待機する条件に関係なく、基本的にそのロックを待機しているすべてのスレッドをウェイクアップします。その結果、一部のスレッドはロックを取得してすぐに取得する可能性があります待機状態が変更されていないことがわかったら、スリープ状態に戻ります)。これは、pthreadのようにきめ細かいスレッド制御を提供しません。しかし、とにかくそれは.Netなので、完全に予想外ではありません。

=============ジョンのリクエストに応じて、ここに改良版があります=============

class BlockingQueue<T>
{
    readonly Queue<T> q = new Queue<T>();

    public void Enqueue(T item)
    {
        lock (q)
        {
            while (false) // condition predicate(s) for producer; can be omitted in this particular case
            {
                System.Threading.Monitor.Wait(q);
            }
            // critical section
            q.Enqueue(item);
        }

        // generally better to signal outside the lock scope
        System.Threading.Monitor.Pulse(q);
    }

    public T Dequeue()
    {
        T t;
        lock (q)
        {
            while (q.Count == 0) // condition predicate(s) for consumer
            {
                System.Threading.Monitor.Wait(q);
            }

            // critical section
            t = q.Dequeue();
        }

        // this can be omitted in this particular case; but not if there's waiting condition for the producer as the producer needs to be woken up; and here's the problem caused by missing condition variable by C# monitor: all threads stay on the same waiting queue of the shared resource/lock.
        System.Threading.Monitor.Pulse(q);

        return t;
    }
}

私が指摘したいいくつかのこと:

1、私のソリューションは、要件と定義をあなたよりも正確に捉えていると思います。具体的には、キューに何も残っていない場合にのみ、コンシューマーを強制的に待機させる必要があります。それ以外の場合、スレッドをスケジュールするのはOS/.Netランタイム次第です。ただし、ソリューションでは、実際に何かを消費したかどうかに関係なく、コンシューマーは各ループで待機する必要があります。これは、私が話していた過度の待機/コンテキストスイッチです。

2、私のソリューションは、コンシューマーコードとプロデューサーコードの両方が同じパターンを共有しているのに対し、あなたのコードはそうではないという意味で対称的です。あなたがパターンを知っていて、この特定のケースのために単に省略されたなら、私はこの点を取り戻します。

3、あなたのソリューションはロックスコープの内側に信号を送りますが、私のソリューションはロックスコープの外側に信号を送ります。ソリューションが悪い理由については、この回答を参照してください。 なぜロックスコープの外に信号を送る必要があるのですか

私はC#モニターで条件変数が欠落しているという欠陥について話していましたが、その影響は次のとおりです。C#が、待機中のスレッドを条件キューからロックキューに移動するソリューションを実装する方法はありません。したがって、過剰なコンテキストスイッチは、リンクの回答によって提案された3スレッドシナリオで発生する運命にあります。

また、条件変数がないため、スレッドが同じ共有リソース/ロックで待機するさまざまなケースを区別できませんが、理由は異なります。すべての待機中のスレッドは、その共有リソースの大きな待機キューに配置されるため、効率が低下します。

「しかし、とにかくそれは.Netなので、完全に予想外ではありません」---.NetがC++ほど高い効率を追求していないことは理解できます、それは理解できます。しかし、それはプログラマーが違いとその影響を知らないはずだという意味ではありません。

于 2015-01-26T16:59:51.823 に答える
4

deadlockempire.github.io/にアクセスします。彼らはあなたが条件変数とロックを理解するのを助けそしてあなたがあなたの望むクラスを書くのを確実に助けるであろう素晴らしいチュートリアルを持っています。

deadlockempire.github.ioで次のコードをステップ実行し、トレースできます。これがコードスニペットです

while (true) {
  Monitor.Enter(mutex);
  if (queue.Count == 0) {
    Monitor.Wait(mutex);
  }
  queue.Dequeue();
  Monitor.Exit(mutex);
}

while (true) {
  Monitor.Enter(mutex);
  if (queue.Count == 0) {
    Monitor.Wait(mutex);
  }
  queue.Dequeue();
  Monitor.Exit(mutex);
}

while (true) {
  Monitor.Enter(mutex);
  queue.Enqueue(42);
  Monitor.PulseAll(mutex);
  Monitor.Exit(mutex);
}
于 2016-03-18T17:40:12.527 に答える
1

h9uestの回答とコメントで指摘されているように、モニターの待機インターフェイスでは適切な条件変数が許可されていません(つまり、共有ロックごとに複数の条件で待機することはできません)。

幸いなことに、.NETの他の同期プリミティブ(SemaphoreSlim、lockキーワード、Monitor.Enter / Exitなど)を使用して、適切な条件変数を実装できます。

次のConditionVariableクラスでは、共有ロックを使用して複数の条件で待機できます。

class ConditionVariable
{
  private int waiters = 0;
  private object waitersLock = new object();
  private SemaphoreSlim sema = new SemaphoreSlim(0, Int32.MaxValue); 

  public ConditionVariable() { 
  }

  public void Pulse() {

      bool release;

      lock (waitersLock)
      {
         release = waiters > 0;
      }

      if (release) {
        sema.Release();
      }
  }

  public void Wait(object cs) {

    lock (waitersLock) {
      ++waiters;
    }

    Monitor.Exit(cs);

    sema.Wait();

    lock (waitersLock) {
      --waiters;
    }

    Monitor.Enter(cs);
  }
}

あなたがする必要があるのは、あなたが待ちたい条件ごとにConditionVariableクラスのインスタンスを作成することです。

object queueLock = new object();

private ConditionVariable notFullCondition = new ConditionVariable();
private ConditionVariable notEmptyCondition = new ConditionVariable();

そして、Monitorクラスの場合と同様に、ConditionVariableのPulseメソッドとWaitメソッドは、同期されたコードブロック内から呼び出す必要があります。

T Take() {

  lock(queueLock) {

    while(queue.Count == 0) {

      // wait for queue to be not empty
      notEmptyCondition.Wait(queueLock);
    }

    T item = queue.Dequeue();

    if(queue.Count < 100) {

      // notify producer queue not full anymore
      notFullCondition.Pulse();
    }

    return item;
  }
}

void Add(T item) {

  lock(queueLock) {

    while(queue.Count >= 100) {

      // wait for queue to be not full
      notFullCondition.Wait(queueLock);
    }

    queue.Enqueue(item);

    // notify consumer queue not empty anymore
    notEmptyCondition.Pulse();
  }
}

以下は、C#で100%マネージコードを使用する適切な条件変数クラスの完全なソースコードへのリンクです。

https://github.com/CodeExMachina/ConditionVariable

于 2016-05-11T13:13:13.217 に答える
0

私は「TheWAY」をの典型的な問題で見つけたと思います

List<string> log; 

複数のスレッドで使用され、一方はそれを埋め、もう一方は処理し、もう一方は空にします

空を避ける

    while(true){
    //stuff
    Thread.Sleep(100)
    }

プログラムで使用される変数

    public static readonly List<string> logList = new List<string>();

    public static EventWaitHandle evtLogListFilled = new AutoResetEvent(false);

プロセッサは次のように機能します

private void bw_DoWorkLog(object sender, DoWorkEventArgs e)
    {
        StringBuilder toFile = new StringBuilder();
        while (true)
        {
            try
            {
                {
                    //waiting form a signal
                    Program.evtLogListFilled.WaitOne();
                    try
                    {
                        //critical section
                        Monitor.Enter(Program.logList);
                        int max = Program.logList.Count;
                        for (int i = 0; i < max; i++)
                        {
                            SetText(Program.logList[0]);
                            toFile.Append(Program.logList[0]);
                            toFile.Append("\r\n");
                            Program.logList.RemoveAt(0);
                        }
                    }
                    finally
                    {
                        Monitor.Exit(Program.logList);
                        // end critical section
                    }


                    try
                    {
                        if (toFile.Length > 0)
                        {
                            Logger.Log(toFile.ToString().Substring(0, toFile.Length - 2));
                            toFile.Clear();
                        }
                    }
                    catch
                    {

                    }
                }
            }
            catch (Exception ex)
            {
                Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), ex);
            }
            Thread.Sleep(100);

        }
    }

フィラースレッドには

public static void logList_add(string str)
    {
        try
        {
            try
            {
                //critical section
                Monitor.Enter(Program.logList);
                Program.logList.Add(str);
            }
            finally
            {
                Monitor.Exit(Program.logList);
                //end critical section
            }
            //set start
            Program.evtLogListFilled.Set();

        }
        catch{}

    }

このソリューションは完全にテストされています。istructionProgram.evtLogListFilled.Set(); Program.evtLogListFilled.WaitOne()のロックと、次の将来のロックを解放する可能性があります。

これが最も簡単な方法だと思います。

于 2017-06-07T13:29:57.330 に答える