条件変数クラスを構築するための私の探求において、私はそれを行うための簡単な方法に出くわしました、そして私はこれをスタックオーバーフローコミュニティと共有したいと思います。私は1時間の大部分をグーグルで検索していましたが、正しいと感じた良いチュートリアルや.NET風の例を実際に見つけることができませんでした。うまくいけば、これは他の人にも役立つでしょう。
6 に答える
lock
とのセマンティクスを理解すれば、実際には非常に簡単ですMonitor
。
ただし、最初に、オブジェクト参照が必要です。を使用できますが、クラスへの参照を持つ誰もがその参照をロックできるという意味で、それthis
を覚えておいてください。これに不安がある場合は、次のように新しいプライベート参照を作成できます。this
public
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.Pulse
Monitor.PulseAll
Monitor.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のクラスを使用してイベント駆動型メッセージシステムを構築し、条件変数を実装することがいかに簡単であるかを説明することです。これがお役に立てば幸いです。
ManualResetEventを使用する
条件変数に似たクラスはManualResetEventですが、メソッド名が少し異なります。
notify_one()
C ++ではSet()
C#で名前が付けられます。C ++ではC#で
名前が付けられます。wait()
WaitOne()
さらに、ManualResetEventReset()
は、イベントの状態を非シグナリングに設定するメソッドも提供します。
受け入れられた答えは良いものではありません。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++ほど高い効率を追求していないことは理解できます、それは理解できます。しかし、それはプログラマーが違いとその影響を知らないはずだという意味ではありません。
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);
}
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%マネージコードを使用する適切な条件変数クラスの完全なソースコードへのリンクです。
私は「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()のロックと、次の将来のロックを解放する可能性があります。
これが最も簡単な方法だと思います。