lock 문 안에 얼마나 많은 작업을해야합니까?


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()lockMonitor.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잠긴 다른 장소 는 in을 SomeMethod(), 감소시키기위한 목적으로 만 _runningWorkers, 그리고 나서 바깥쪽에 foreach있는 숫자는 _runningWorkers0 이되기를 기다렸다가 로깅 및 리턴합니다.

도움을 주셔서 감사합니다.

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의 Russell

@RussellatISC : ThreadPool.QueueUserWorkItem은 SomeMethod비동기 적으로 호출 합니다. 위의 "잠금"섹션은 새 스레드가 SomeMethod실행되기 시작하기 직전 또는 그 직후에 남아 있습니다.
Doc Brown

좋은 지적. Monitor.Wait()다른 리소스 ( SomeMethod이 경우)가 잠금을 사용할 수 있도록 잠금을 해제하고 다시 얻는 것이 목적이라는 것을 이해하고 있습니다. 다른 한편으로, SomeMethod잠금을 획득하고 카운터를 감소시킨 다음 Monitor.Pulse()해당 메소드에 잠금을 리턴하는 호출을 호출 합니다. 다시, 이것은 내 자신의 이해입니다.
Joseph

@Doc은 그 점을 놓쳤지만 여전히 ... 다음 반복에서 foreach가 잠기기 전에 SomeMethod를 시작해야하거나 "_runningWorkers> = MaxSimultaneousThreads"동안 보류 된 잠금에 대해 중단 된 것처럼 보입니다.
ISC의 Russell

@RussellatISC : Joseph이 이미 언급했듯이 Monitor.Wait잠금을 해제합니다. 문서를 살펴 보는 것이 좋습니다.
Doc Brown

답변:


33

이것은 성능 문제가 아닙니다. 그것은 무엇보다도 정확성의 문제입니다. 두 개의 잠금 문이있는 경우 이들 사이에 또는 부분적으로 잠금 문 외부에 분산 된 조작에 대해서는 원 자성을 보장 할 수 없습니다. 이전 버전의 코드에 맞게 다음을 의미합니다.

의 끝 사이 while (_runningWorkers >= MaxSimultaneousThreads)_runningWorkers++, 전혀 아무것도 발생할 수 있습니다 코드 항복 사이에 잠금 장치를 재 - 획득하기 때문에. 예를 들어, 스레드 A가 처음으로 잠금을 획득하고 다른 스레드가 종료 될 때까지 기다린 다음 루프와를 종료 할 수 lock있습니다. 그런 다음 선점되고 스레드 B가 사진에 들어가 스레드 스레드의 공간을 기다립니다. 말했다 다른 스레드가 종료되기 때문에, 거기 이다 매우 긴 전혀 대기하지 않도록 방. 나사산 A와 나사산 B는 이제 각각 순서대로 증가 _runningWorkers하고 작업을 시작합니다.

이제는 내가 볼 수있는 한 데이터 레이스가 없지만 논리적으로 잘못되어 있습니다. 왜냐하면 MaxSimultaneousThreads작업자가 더 많기 때문 입니다. 스레드 풀에서 슬롯을 가져 오는 작업이 원자 적이 지 않기 때문에 검사는 (때로는) 효과가 없습니다. 이것은 잠금 세분성에 대한 작은 최적화 이상의 것입니다! 반대로, 너무 일찍 또는 너무 오래 잠그면 교착 상태가 발생할 수 있습니다.

두 번째 스 니펫은 내가 볼 수있는 한이 문제를 해결합니다. 이 문제를 해결하려면 덜 침습적 인 변화는 퍼팅 수 있습니다 ++_runningWorkers애프터 권리를 while최초의 잠금 문 내부 모습.

이제 정확성은 제쳐두고 성능은 어떻습니까? 이것은 말하기 어렵다. 일반적으로 더 긴 시간 동안 잠금을 설정하면 ( "거친") 동시성이 억제되지만 세밀한 잠금의 추가 동기화로 인한 오버 헤드와 균형을 맞출 필요가 있습니다. 일반적으로 유일한 솔루션은 벤치마킹하고 "모든 곳에서 모든 것을 잠그고"그리고 "최소한 만 잠그는 것"보다 더 많은 옵션이 있다는 것을 인식하고 있습니다. 풍부한 패턴과 동시성 프리미티브 및 스레드 안전 데이터 구조를 사용할 수 있습니다. 예를 들어, 이것은 응용 프로그램 세마포어가 고안된 것처럼 보이므로이 수동 롤 카운터 대신에 세마포어를 사용하는 것이 좋습니다.


11

IMHO 당신은 잘못된 질문을하고 있습니다-당신은 효율성 트레이드 오프에 너무 신경 쓰지 말고 정확성에 더 관심을 가져야합니다.

첫 번째 변형은 _runningWorkers잠금 중에 만 액세스 해야 하지만 _runningWorkers첫 번째 잠금과 두 번째 잠금 사이의 갭에서 다른 스레드에 의해 변경 될 수 있는 경우를 놓칩니다 . 솔직히, 누군가가 _runningWorkers암시와 잠재적 오류에 대해 생각하지 않고 모든 액세스 포인트를 맹목적으로 잠그면 코드가 나에게 보입니다 . 아마도 저자는 블록 break안에서 문장을 실행하는 것에 대해 미신적 인 두려움을 가지고 있었을 지 lock모르지만 누가 알겠습니까?

따라서 실제로 두 번째 변형을 사용하는 것이 다소 효율적이지 않지만 첫 번째 변형보다 (정확히) 더 정확하기 때문에 사용해야합니다.


반대로, 다른 잠금을 획득해야하는 작업을 수행하는 동안 잠금을 유지하면 교착 상태가 발생하여 "올바른"동작이라고 할 수 없습니다. 장치로 수행해야하는 모든 코드가 공통 잠금으로 둘러싸여 있는지 확인해야하지만, 해당 장치의 일부일 필요가없는 잠금 장치, 특히 다른 잠금 장치를 획득해야하는 잠금 장치 외부로 이동해야합니다. .
supercat

@ supercat : 여기에 해당되지 않습니다. 원래 질문 아래의 의견을 읽으십시오.
Doc Brown

9

다른 답변은 상당히 좋고 정확성 문제를 명확하게 해결합니다. 더 일반적인 질문에 대해 말씀 드리겠습니다.

lock 문 안에 얼마나 많은 작업을해야합니까?

수락 된 답변의 마지막 단락에서 암시하고 delnan이 암시하는 표준 조언으로 시작합시다.

  • 특정 물체를 잠그는 동안 가능한 적은 작업을 수행하십시오. 오랫동안 보유한 잠금은 경합의 대상이되며 경합이 느립니다. 이는 것을 의미합니다 A의 코드의 양을 특정 잠금 장치와 전체 코드의 양이 동일한 개체에 잠금 모든 잠금 문이 모두 해당됩니다.

  • 교착 상태 (또는 라이브 록)의 가능성을 낮추려면 가능한 한 적은 수의 잠금을 유지하십시오.

영리한 독자는 이것들이 반대라는 것을 알게 될 것입니다. 첫 번째 요점은 경합을 피하기 위해 큰 자물쇠를 더 작고 세밀한 자물쇠로 나누는 것입니다. 두 번째는 교착 상태를 피하기 위해 고유 한 잠금을 동일한 잠금 오브젝트에 통합하는 것을 제안합니다.

최상의 표준 조언이 완전히 모순된다는 사실에서 무엇을 결론 낼 수 있습니까? 실제로 좋은 조언을 얻습니다.

  • 처음에는 거기에 가지 마십시오. 스레드간에 메모리를 공유하는 경우 고통의 세계를 열 수 있습니다.

내 조언은 동시성을 원한다면 프로세스를 동시 단위로 사용하는 것입니다. 프로세스를 사용할 수없는 경우 응용 프로그램 도메인을 사용하십시오. 응용 프로그램 도메인을 사용할 수없는 경우 작업 병렬 라이브러리에서 스레드를 관리하고 하위 수준 스레드 (작업자)가 아닌 고급 작업 (작업)의 관점에서 코드를 작성하십시오.

스레드 또는 세마포어와 같은 저수준 동시성 프리미티브를 절대적으로 사용해야하는 경우이를 사용하여 실제로 필요한 것을 캡처하는 고수준 추상화 를 작성하십시오. 더 높은 수준의 추상화는 "사용자가 취소 할 수있는 작업을 비동기식으로 수행"과 같은 것으로, TPL은 이미이를 지원하므로 사용자가 직접 롤링 할 필요가 없습니다. 스레드 안전 지연 초기화와 같은 것이 필요할 것입니다. Lazy<T>전문가가 작성한을 사용하지 마십시오 . 전문가가 작성한 스레드 세이프 컬렉션 (불변 또는 기타)을 사용하십시오. 추상화 수준을 최대한 높이십시오.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.