델파이의 스레드 클래스
랩터[멘탈스튜디오]
http://mental.mentu.com
4개
크리티컬 섹션(CriticalSection)은 공유 데이터 접근 보호를 위한 기술입니다. 이는 실제로 전역 부울 변수와 동일합니다. 그러나 해당 작업은 Enter 및 Leave라는 두 가지 작업만 가지고 있으며 두 가지 상태는 각각 임계 영역에 있는지 여부를 나타내는 True 및 False로 간주될 수 있습니다. 이 두 작업은 기본 작업이므로 다중 스레드 응용 프로그램의 액세스 위반으로부터 공유 데이터를 보호하는 데 사용할 수 있습니다.
공유 데이터를 보호하기 위해 임계 섹션을 사용하는 방법은 매우 간단합니다. 공유 데이터에 매번 액세스하기 전에 Enter를 호출하여 임계 섹션 플래그를 설정한 다음 데이터를 조작하고 마지막으로 Leave를 호출하여 임계 섹션을 종료합니다. 보호 원칙은 다음과 같습니다. 스레드가 임계 섹션에 들어간 후 다른 스레드도 이때 데이터에 액세스하려는 경우 Enter를 호출할 때 스레드가 이미 임계 섹션에 들어간 것을 발견하고 이 스레드는 다음과 같습니다. 전화를 끊고 현재 임계 섹션에 있는 스레드가 Leave를 호출하여 임계 섹션을 떠날 때까지 기다립니다. 다른 스레드가 작업을 완료하고 Leave를 호출하여 떠나면 이 스레드가 깨어나 임계 섹션 플래그를 설정하고 데이터 작업을 시작합니다. 따라서 충돌을 방지합니다.
이전 InterlockedIncrement를 예로 들어 CriticalSection(Windows API)을 사용하여 구현합니다.
바르
InterlockedCrit: TRTLCriticalSection;
PRocedure InterlockedIncrement( var aValue : Integer );
시작하다
EnterCriticalSection(InterlockedCrit);
Inc(a값);
LeaveCriticalSection(InterlockedCrit);
끝;
이제 이전 예를 살펴보겠습니다.
1. 스레드 A가 임계 섹션에 들어갑니다(데이터가 3이라고 가정).
2. 스레드 B가 임계 섹션에 들어갑니다. A가 이미 임계 섹션에 있으므로 B는 일시 중단됩니다.
3. 스레드 A는 데이터에 1을 추가합니다(현재 4).
4. 스레드 A가 임계 섹션을 떠나 스레드 B를 깨웁니다(현재 메모리의 데이터는 4입니다).
5. 스레드 B가 깨어나서 데이터에 1을 추가합니다(현재는 5입니다).
6. 스레드 B가 임계 섹션을 벗어나고 현재 데이터가 정확합니다.
이것이 중요한 섹션이 공유 데이터에 대한 액세스를 보호하는 방법입니다.
중요 섹션 사용과 관련하여 주목해야 할 한 가지는 데이터 액세스 중 예외 처리입니다. 데이터 연산 중에 예외가 발생하면 Leave 연산이 실행되지 않기 때문에 깨워야 할 스레드가 깨어나지 않아 프로그램이 응답하지 않을 수 있기 때문입니다. 따라서 일반적으로 올바른 접근 방식은 다음과 같이 중요한 섹션을 사용하는 것입니다.
EnterCriticalSection
노력하다
// 크리티컬 섹션 데이터 연산
마지막으로
중요 섹션 떠나기
끝;
마지막으로 주목해야 할 점은 Event와 CriticalSection은 모두 운영 체제 리소스이므로 사용 전에 생성하고 사용 후 해제해야 한다는 것입니다. 예를 들어 TThread 클래스에서 사용하는 전역 이벤트: SyncEvent 및 전역 CriticalSection: TheadLock은 모두 InitThreadSynchronization 및 DoneThreadSynchronization에서 생성 및 해제되며 클래스 유닛의 초기화 및 마무리에서 호출됩니다.
TThread에서는 Event와 CriticalSection을 동작시키기 위해 API가 사용되므로, 실제로 Delphi에서는 이를 캡슐화한 TEvent 클래스와 TCriticalSection 클래스를 제공합니다. 사용법은 이전 API 사용방법과 거의 동일합니다. TEvent의 생성자에는 매개변수가 너무 많기 때문에 단순화를 위해 Delphi는 기본 매개변수로 초기화된 Event 클래스인 TSimpleEvent도 제공합니다.
그런데 스레드 동기화에 사용되는 또 다른 클래스인 TMultiReadExclusiveWriteSynchronizer를 소개하겠습니다. 이 클래스는 SysUtils 유닛에 정의되어 있습니다. 내가 아는 한, 이는 Delphi RTL에 정의된 가장 긴 클래스 이름입니다. 다행히도 TMREWSync라는 짧은 별칭이 있습니다. 사용법에 대해서는 이름만 봐도 알 수 있을 것 같아서 더 이상 말하지 않겠습니다.
Event 및 CriticalSection에 대한 이전 준비 지식을 바탕으로 공식적으로 동기화 및 WaitFor에 대한 논의를 시작할 수 있습니다.
우리는 동기화가 실행을 위해 코드의 일부를 메인 스레드에 배치하여 스레드 동기화를 달성한다는 것을 알고 있습니다. 프로세스에는 메인 스레드가 하나만 있기 때문입니다. 먼저 동기화 구현을 살펴보겠습니다.
절차 TThread.Synchronize(메소드: TThreadMethod);
시작하다
FSynchronize.FThread := 자기;
FSynchronize.FSynchronizeException := nil;
FSynchronize.FMethod := 메서드;
동기화(@FSynchronize);
끝;
여기서 FSynchronize는 레코드 유형입니다.
PSynchronizeRecord = ^TSynchronizeRecord;
TSynchronizeRecord = 기록
FThread: TObject;
FMethod: TThread메소드;
FSynchronizeException: TObject;
끝;
들어오는 스레드 클래스 개체, 동기화 방법 및 발생하는 예외를 포함하여 스레드와 기본 스레드 간의 데이터 교환에 사용됩니다.
오버로드된 버전은 동기화에서 호출되며 이 오버로드된 버전은 매우 특별하며 "클래스 메서드"입니다. 소위 클래스 메소드는 호출 시 클래스 인스턴스를 생성할 필요가 없지만 생성자와 같이 클래스 이름을 통해 호출되는 특수 클래스 멤버 메소드입니다. 클래스 메소드를 이용하여 구현한 이유는 스레드 객체가 생성되지 않은 상태에서도 호출이 가능하기 때문이다. 그러나 실제로는 또 다른 오버로드된 버전(클래스 메서드이기도 함)과 또 다른 클래스 메서드인 StaticSynchronize가 사용됩니다. 이 동기화에 대한 코드는 다음과 같습니다.
클래스 프로시저 TThread.Synchronize(ASyncRec: PSynchronizeRecord);
var
동기화프록: TSyncProc;
시작하다
GetCurrentThreadID = MainThreadID인 경우
ASyncRec.F방법
또 다른
시작하다
SyncProc.Signal := CreateEvent(nil, True, False, nil);
노력하다
EnterCriticalSection(ThreadLock);
노력하다
SyncList = nil이면
SyncList := TList.Create;
SyncProc.SyncRec := ASyncRec;
SyncList.Add(@SyncProc);
SignalSync이벤트;
할당된 경우(WakeMainThread) 다음
WakeMainThread(SyncProc.SyncRec.FThread);
LeaveCriticalSection(ThreadLock);
노력하다
WaitForSingleObject(SyncProc.Signal, INFINITE);
마지막으로
EnterCriticalSection(ThreadLock);
끝;
마지막으로
LeaveCriticalSection(ThreadLock);
끝;
마지막으로
CloseHandle(SyncProc.Signal);
끝;
Assigned(ASyncRec.FSynchronizeException)인 경우 ASyncRec.FSynchronizeException을 발생시킵니다.
끝;
끝;
이 코드는 조금 길지만 너무 복잡하지는 않습니다.
첫 번째는 현재 스레드가 메인 스레드인지 확인하는 것입니다. 그렇다면 간단히 동기화 메서드를 실행하고 반환하면 됩니다.
메인 스레드가 아닌 경우 동기화 프로세스를 시작할 준비가 된 것입니다.
스레드 교환 데이터(매개변수) 및 이벤트 핸들은 로컬 변수 SyncProc을 통해 기록됩니다. 기록 구조는 다음과 같습니다.
TSyncProc=기록
SyncRec: PSynchronizeRecord;
신호: THandle;
끝;
그런 다음 이벤트를 생성한 다음 임계 섹션에 들어가고(전역 변수 ThreadLock을 통해, 동시에 하나의 스레드만 동기화 상태에 들어갈 수 있으므로 전역 변수를 사용하여 기록할 수 있으므로) 기록된 데이터를 SyncList 목록(목록이 없으면 새로 만듭니다). ThreadLock의 중요한 부분은 SyncList에 대한 액세스를 보호하는 것임을 알 수 있습니다. 이는 나중에 CheckSynchronize가 도입될 때 다시 볼 수 있습니다.
다음 단계는 SignalSyncEvent를 호출하는 것입니다. 해당 코드는 이전에 TThread 생성자를 도입할 때 소개되었으며 해당 기능은 단순히 SyncEvent에서 Set 작업을 수행하는 것입니다. 이 SyncEvent의 목적은 나중에 WaitFor가 도입될 때 자세히 설명됩니다.
다음은 가장 중요한 부분입니다. 동기화 작업을 위해 WakeMainThread 이벤트를 호출하는 것입니다. WakeMainThread는 TNotifyEvent 유형의 전역 이벤트입니다. 여기에서 이벤트를 처리하는 데 사용되는 이유는 동기화 메서드가 기본적으로 메시지를 통해 실행하기 위해 동기화해야 하는 프로세스를 기본 스레드에 넣기 때문입니다. 메시지 루프가 없는 일부 응용 프로그램(예: 콘솔 또는 DLL)에서는 사용할 수 없습니다. 이므로 이 이벤트를 처리에 사용하세요.
애플리케이션 객체는 이 이벤트에 응답합니다. 다음 두 가지 메서드는 WakeMainThread 이벤트(Forms 유닛에서)에 대한 응답을 설정하고 지우는 데 사용됩니다.
절차 TApplication.HookSynchronizeWakeup;
시작하다
Classes.WakeMainThread := WakeMainThread;
끝;
절차 TApplication.UnhookSynchronizeWakeup;
시작하다
Classes.WakeMainThread := nil;
끝;
위의 두 메소드는 각각 TApplication 클래스의 생성자와 소멸자에서 호출됩니다.
이것은 Application 객체의 WakeMainThread 이벤트에 응답하는 코드입니다. 메시지는 여기로 전송됩니다. 이를 위해 빈 메시지가 사용됩니다.
절차 TApplication.WakeMainThread(Sender: TObject);
시작하다
PostMessage(핸들, WM_NULL, 0, 0);
끝;
이 메시지에 대한 응답은 Application 개체에도 있습니다. 다음 코드를 참조하세요(관련 없는 부분 제거).
절차 TApplication.WndProc(var 메시지: TMessage);
…
시작하다
노력하다
…
메시지와 함께
사례 메시지
…
WM_NULL:
동기화를 확인하세요.
…
제외하고
HandleException(자기);
끝;
끝;
그 중 CheckSynchronize도 Classes 유닛에 정의되어 있는데, 상대적으로 복잡하기 때문에 당분간은 자세히 설명하지 않겠습니다. 이제 동기화 기능을 구체적으로 처리하는 부분이라는 점만 알아두시기 바랍니다. 암호.
WakeMainThread 이벤트를 실행한 후 임계 섹션을 종료한 다음 WaitForSingleObject를 호출하여 임계 섹션에 들어가기 전에 생성된 이벤트를 기다리기 시작합니다. 이 이벤트의 기능은 이 동기화 메서드의 실행이 끝날 때까지 기다리는 것입니다. 이에 대해서는 나중에 CheckSynchronize를 분석할 때 설명하겠습니다.
WaitForSingleObject 후에 임계 섹션에 다시 들어가지만 아무 것도 하지 않고 종료됩니다. 의미 없는 것처럼 보이지만 꼭 필요합니다.
중요한 섹션의 Enter 및 Leave는 일대일로 엄격하게 일치해야 하기 때문입니다. 그러면 다음과 같이 바뀔 수 있나요?
할당된 경우(WakeMainThread) 다음
WakeMainThread(SyncProc.SyncRec.FThread);
WaitForSingleObject(SyncProc.Signal, INFINITE);
마지막으로
LeaveCriticalSection(ThreadLock);
끝;
위 코드와 원본 코드의 가장 큰 차이점은 WaitForSingleObject도 임계 영역의 제한 사항에 포함된다는 점입니다. 아무런 영향도 없을 것 같고, 코드도 크게 단순화되는데, 정말 가능할까요?
사실, 아니오!
Enter 임계 섹션 이후에 다른 스레드가 다시 들어가려고 하면 일시 중단된다는 것을 알고 있기 때문입니다. WaitFor 메서드는 현재 스레드를 일시 중단하고 다른 스레드의 SetEvent를 기다릴 때까지 깨어나지 않습니다. 위와 같이 코드를 변경하면 SetEvent 스레드도 임계 영역에 진입해야 하는 경우 교착 상태가 발생합니다(교착 상태 이론은 운영 체제 원리 정보를 참조하십시오).
교착 상태는 스레드 동기화의 가장 중요한 측면 중 하나입니다!
마지막으로 처음에 생성된 Event가 해제됩니다. 동기화된 메서드가 예외를 반환하면 여기서 다시 예외가 발생합니다.
(계속 예정)