私は、サードパーティのソリューションからデータを受け取り、それをデータベースに保存し、別のサードパーティのソリューションで使用できるようにデータを調整するソフトウェアの更新を作成する作業をしているジュニア開発者です。私たちのソフトウェアはWindowsサービスとして実行されます。
以前のバージョンのコードを見ると、次のことがわかります。
_ static Object _workerLocker = new object();
static int _runningWorkers = 0;
int MaxSimultaneousThreads = 5;
foreach(int SomeObject in ListOfObjects)
{
lock (_workerLocker)
{
while (_runningWorkers >= MaxSimultaneousThreads)
{
Monitor.Wait(_workerLocker);
}
}
// check to see if the service has been stopped. If yes, then exit
if (this.IsRunning() == false)
{
break;
}
lock (_workerLocker)
{
_runningWorkers++;
}
ThreadPool.QueueUserWorkItem(SomeMethod, SomeObject);
}
_
ロジックは明確に思われます。スレッドプールの空き容量を待って、サービスが停止していないことを確認してから、スレッドカウンターをインクリメントして作業をキューに入れます。 __runningWorkers
_はSomeMethod()
内でlock
ステートメント内でデクリメントされ、次にMonitor.Pulse(_workerLocker)
を呼び出します。
私の質問は:次のように、すべてのコードを単一のlock
内にグループ化することには利点がありますか?
_ static Object _workerLocker = new object();
static int _runningWorkers = 0;
int MaxSimultaneousThreads = 5;
foreach (int SomeObject in ListOfObjects)
{
// Is doing all the work inside a single lock better?
lock (_workerLocker)
{
// wait for room in ThreadPool
while (_runningWorkers >= MaxSimultaneousThreads)
{
Monitor.Wait(_workerLocker);
}
// check to see if the service has been stopped.
if (this.IsRunning())
{
ThreadPool.QueueUserWorkItem(SomeMethod, SomeObject);
_runningWorkers++;
}
else
{
break;
}
}
}
_
他のスレッドを待つのに少し時間がかかるように見えますが、単一の論理ブロックで繰り返しロックするのも時間がかかるようです。ただし、私はマルチスレッドに不慣れであるため、ここでは他にも気づいていない懸念があると思います。
__workerLocker
_がロックされる他の唯一の場所はSomeMethod()
内であり、__runningWorkers
_をデクリメントする目的でのみ、次にforeach
の外側で数値を待機しますの__runningWorkers
_は、ログに記録して戻る前にゼロになります。
助けてくれてありがとう。
編集4/8/15
セマフォの使用を推奨してくれた@delnanに感謝します。コードは次のようになります。
_ static int MaxSimultaneousThreads = 5;
static Semaphore WorkerSem = new Semaphore(MaxSimultaneousThreads, MaxSimultaneousThreads);
foreach (int SomeObject in ListOfObjects)
{
// wait for an available thread
WorkerSem.WaitOne();
// check if the service has stopped
if (this.IsRunning())
{
ThreadPool.QueueUserWorkItem(SomeMethod, SomeObject);
}
else
{
break;
}
}
_
WorkerSem.Release()
はSomeMethod()
内で呼び出されます。
これはパフォーマンスの問題ではありません。それは何よりもまず正しさの問題です。 2つのロックステートメントがある場合、それらのステートメント間または部分的にロックステートメントの外側にある操作の原子性を保証することはできません。コードの古いバージョンに合わせて調整すると、これは次のことを意味します。
while (_runningWorkers >= MaxSimultaneousThreads)
の終わりと_runningWorkers++
の終わりの間では、コードが引き渡されてその間のロックを再取得するため、何でも起こりますになります。たとえば、スレッドAが初めてロックを取得し、他のスレッドが終了するまで待機してから、ループとlock
を抜け出す場合があります。次に、スレッドBがプリエンプトされ、スレッドBが画像に入り、スレッドプールのスペースを待ちます。他のスレッドが終了したため、isの空きがあるため、非常に長く待機しません。スレッドAとスレッドBの両方が順番に進み、それぞれが_runningWorkers
をインクリメントして作業を開始します。
今、私が見る限りではデータの競合はありませんが、logicallyMaxSimultaneousThreads
を超えるワーカーが存在するため、これは誤りですランニング。スレッドプールのスロットを取得するタスクはアトミックではないため、このチェックは(場合によっては)効果がありません。これは、ロックの細分性に関する小さな最適化以上のものに関係するはずです! (逆に、ロックが早すぎたり長すぎたりすると、簡単にデッドロックが発生する可能性があります。)
2番目のスニペットは、私が見る限り、この問題を修正します。問題を修正するための侵襲性の少ない変更は、while
ルックの直後の最初のロックステートメント内に++_runningWorkers
を置くことです。
さて、正しさはさておき、パフォーマンスはどうですか?これはわかりにくいです。通常、より長い時間(「粗く」)ロックすると並行性が阻害されますが、細かいロックの追加の同期によるオーバーヘッドとバランスを取る必要があります。一般的に唯一の解決策はベンチマークであり、「どこでもすべてをロックする」および「最低限のみロックする」よりも多くのオプションがあることを認識しています。豊富なパターンと並行処理プリミティブ、およびスレッドセーフなデータ構造が利用可能です。たとえば、これはまさにアプリケーションセマフォが発明されたように思われるので、この手巻きハンドロックカウンターの代わりにそれらの1つを使用することを検討してください。
あなたが間違った質問をしている私見-あなたは効率のトレードオフについてそれほど気にすべきではなく、正確さについてもっと気にするべきです。
最初のバリアントは_runningWorkers
がロック中にのみアクセスされることを確認しますが、最初のロックと2番目のロックの間のギャップにある別のスレッドによって_runningWorkers
が変更される可能性があるケースを逃します。正直なところ、コードは私に、誰かが_runningWorkers
のすべてのアクセスポイントを盲目的にロックした場合、影響と潜在的なエラーを考慮せずに見えます。おそらく、著者はbreak
block内でlock
ステートメントを実行することについて迷信的な懸念を抱いていましたが、誰が知っていますか?
したがって、2番目のバリアントを実際に使用する必要があります。多かれ少なかれ効率が良いからではなく、(うまくいけば)最初のものよりも正しいからです。
他の答えは非常によく、正確性の懸念に明確に対処します。より一般的な質問をさせてください:
ロックステートメント内にどのくらいの作業を配置する必要がありますか?
受け入れられた回答の最後の段落であなたが暗示し、デルナンが暗示する標準的なアドバイスから始めましょう:
特定のオブジェクトをロックしている間は、できる限り作業を行わないでください。長期間保持されているロックは競合の影響を受けやすく、競合は低速です。これは、totalparticularロックのコード量とtotalall lockステートメントのコード量)同じオブジェクトのロックは両方に関連します。
デッドロック(またはライブロック)の可能性を低くするために、ロックの数はできるだけ少なくしてください。
賢い読者なら、これらは反対であることに気付くでしょう。最初の点は、競合を回避するために、大きなロックを多数のより細かい粒度のロックに分割することを示唆しています。 2番目は、デッドロックを回避するために、個別のロックを同じロックオブジェクトに統合することを提案しています。
最高の標準的なアドバイスが完全に矛盾しているという事実から、私たちは何を結論付けることができますか?私たちは実際に良いアドバイスを得ます:
私のアドバイスは、並行性が必要な場合は、プロセスを並行性の単位として使用することです。プロセスを使用できない場合は、アプリケーションドメインを使用します。アプリケーションドメインを使用できない場合は、タスクパラレルライブラリでスレッドを管理し、低レベルのスレッド(ワーカー)ではなく高レベルのタスク(ジョブ)の観点からコードを記述します。
スレッドやセマフォなどの低レベルの同時実行プリミティブを絶対的に積極的に使用する必要がある場合は、それらを使用して、より高いレベルの抽象化を構築する本当に必要なものをキャプチャします。より高いレベルの抽象化は「ユーザーがキャンセルできるタスクを非同期で実行する」ようなものであることに気付くでしょう。TPLはすでにそれをサポートしているので、自分でロールする必要はありません。スレッドセーフな遅延初期化のようなものが必要になることに気付くでしょう。自分で振るのではなく、Lazy<T>
、専門家によって書かれました。専門家によって書かれたスレッドセーフなコレクション(不変またはその他)を使用します。抽象化レベルを可能な限り高くします。