ロック文の中にどれくらいの作業を配置する必要がありますか?


27

私は、サードパーティソリューションからデータを受信し、データベースに保存し、別のサードパーティソリューションで使用するためにデータを調整するソフトウェアの更新プログラムの作成に取り組んでいるジュニア開発者です。当社のソフトウェアは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()ます。


1
ブロック全体がロックされている場合、SomeMethodは_runningWorkersを減らすためにどのようにロックを取得しますか?
ラッセルISC

@RussellatISC:ThreadPool.QueueUserWorkItemはSomeMethod非同期で呼び出します。上記の「ロック」セクションは、新しいスレッドSomeMethodが実行を開始する前、または少なくともすぐ後に残されます。
Doc Brown

いい視点ね。Monitor.Wait()別のリソース(SomeMethodこの場合は)がロックを使用できるように、ロックを解除して再取得することが目的であると理解しています。反対側でSomeMethodは、ロックを取得し、カウンターをデクリメントしてMonitor.Pulse()から、問題のメソッドにロックを返す呼び出しを行います。繰り返しますが、これは私自身の理解です。
ジョセフ

@Docはそれを逃しましたが、それでも...次の反復でforeachがロックされる前にSomeMethodを開始する必要があるか、「while(_runningWorkers> = MaxSimultaneousThreads)」で保持されているロックに対してハングしているようです。
ラッセルISC

@RussellatISC:ジョセフがすでに述べたようにMonitor.Wait、ロックを解除します。ドキュメントをご覧になることをお勧めします。
ドックブラウン

回答:


33

これはパフォーマンスの問題ではありません。それは何よりもまず正しさの問題です。2つのロックステートメントがある場合、それらの間または部分的にロックステートメントの外側に分散する操作の原子性を保証することはできません。コードの古いバージョンに合わせて調整されているため、次のことを意味します。

端部との間while (_runningWorkers >= MaxSimultaneousThreads)_runningWorkers++すべてのものが発生する可能性があり、コードの解約との間でロックを再取得しているため。たとえば、スレッドAは初めてロックを取得し、他のスレッドが終了するまで待機してから、ループとを抜けますlock。その後、プリエンプション処理が行われ、スレッドBが写真に入り、スレッドプールの空きを待ちます。他のスレッドが終了したので、そこにある、それはすべてで非常に長く待つことはありませんので、部屋が。スレッドAとスレッドBの両方が順番に進み、それぞれ_runningWorkersが作業を増やして開始します。

今、私が見ることができる限りデータ競合はありませんが、論理的には間違っMaxSimultaneousThreadsています。スレッドプールでスロットを取得するタスクはアトミックではないため、チェックは(ときどき)無効です。これは、ロックの粒度に関する小さな最適化以上のものに関係するはずです!(逆に、ロックが早すぎるか、長すぎると、簡単にデッドロックにつながる可能性があることに注意してください。)

私が見る限り、2番目のスニペットはこの問題を修正します。問題を修正するためのより侵襲性の低い変更は、最初のロックステートメント内に外観の++_runningWorkers直後を配置することwhileです。

さて、正確さは別として、パフォーマンスはどうでしょうか?これはわかりにくいです。一般に、長時間(「粗い」)のロックは同時実行性を抑制しますが、あなたが言うように、これはきめ細かいロックの追加の同期によるオーバーヘッドとバランスを取る必要があります。一般的に、唯一の解決策は、ベンチマークを行い、「どこでもすべてをロックする」や「最低限のみロックする」よりも多くのオプションがあることを認識することです。豊富なパターンと並行処理プリミティブ、およびスレッドセーフなデータ構造が利用可能です。たとえば、これはまさにアプリケーションセマフォが発明されたように思えるので、この手巻きハンドロックカウンタの代わりにそれらの1つを使用することを検討してください。


11

私見あなたは間違った質問をしている-あなたは効率のトレードオフについてはそれほど気にするべきではないが、正確さについてはもっとすべきだ。

最初のバリアントは_runningWorkers、ロック中にのみアクセスされることを確認します_runningWorkersが、最初のロックと2番目のロックの間のギャップで別のスレッドによって変更される可能性があるケースを逃します。正直なところ、コードは_runningWorkers、影響や潜在的なエラーを考えずに、すべてのアクセスポイントの周りに誰かが盲目的にロックをかけた場合に私に見えます。作者はブロックbreak内でステートメントを実行することに迷信を抱いているかもしれませlockんが、誰が知っていますか?

そのため、実際には2番目のバリアントを使用する必要があります。これは多かれ少なかれ効率的ではなく、最初のバリアントよりも(できれば)より正確であるためです。


反対に、別のロックの取得が必要なタスクを引き受けながらロックを保持すると、「正しい」動作とはほとんど言えないデッドロックが発生する可能性があります。ユニットとして実行する必要があるすべてのコードが共通のロックで囲まれていることを確認する必要がありますが、そのユニットの一部である必要のないロック、特に他のロックの取得が必要なロックの外側に移動する必要があります。
-supercat

@supercat:これはここでは当てはまりません。元の質問の下のコメントを読んでください。
Doc Brown

9

他の答えは非常に良く、正確性の懸念に明確に対処しています。より一般的な質問に答えましょう。

ロック文の中にどれくらいの作業を配置する必要がありますか?

一般的なアドバイスから始めましょう。受け入れられた答えの最後のパラグラフで、あなたがほのめかし、デルナンがほのめかしています。

  • 特定のオブジェクトをロックしている間は、できるだけ作業を少なくします。長時間保持されているロックは競合の対象となり、競合は遅くなります。このことを意味することに留意されたい合計のコードの量の特定のロックおよび合計で符号量同じオブジェクトのロック全てロックステートメントは両方とも関連しています。

  • デッドロック(またはライブロック)の可能性を低くするために、ロックをできるだけ少なくします。

賢い読者は、これらが正反対であることを認識するでしょう。最初のポイントは、競合を回避するために、大きなロックを多くの小さな、よりきめの細かいロックに分割することを提案します。2番目の例では、デッドロックを回避するために、異なるロックを同じロックオブジェクトに統合することを提案しています。

最高の標準アドバイスが完全に矛盾しているという事実から、私たちは何を結論づけることができますか?実際に良いアドバイスを得ることができます。

  • そもそもそこに行かないでください。スレッド間でメモリを共有している場合、あなたは苦痛の世界に自分自身を開放しています。

私のアドバイスは、並行性が必要な場合、並行性の単位としてプロセスを使用することです。プロセスを使用できない場合は、アプリケーションドメインを使用します。アプリケーションドメインを使用できない場合は、タスクパラレルライブラリでスレッドを管理し、低レベルのスレッド(ワーカー)ではなく高レベルのタスク(ジョブ)の観点からコードを記述します。

スレッドやセマフォなどの低レベルの同時実行プリミティブを絶対に使用する必要がある場合は、それらを使用して、本当に必要なものをキャプチャする高レベルの抽象化を構築します。高レベルの抽象化は、「ユーザーがキャンセルできるタスクを非同期に実行する」ようなものであることに気付くでしょう。また、TPLはすでにそれをサポートしているので、独自にロールする必要はありません。スレッドセーフな遅延初期化のようなものが必要であることに気付くでしょう。Lazy<T>専門家によって書かれた独自のuseを使用しないでください。専門家が作成したスレッドセーフコレクション(不変またはその他)を使用します。抽象化のレベルをできるだけ高くします。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.