Multi-threading은 프로그램을 하나 돌렸을 때 CPU 안에서 일을 분산하여 돌리는 작업을 의미한다. 여기서 프로그램 하나를 돌리는 것은 프로세스(process) 그리고 분산된 작업 하나의 단위를 스레드(thread) 라고 한다. 즉 프로세스는 하나인데 스레드를 여러개 만들어서 분산 작업을 한다.
작동 원리
다음과 같은 조건을 들어보자.
- 10개의 스레드 사용
- 10000개의 이벤트 수행
- (Run Action, Primary Generator Action, Stepping Action) 3개의 클래스 사용.
간단하게 Action Class 묶음 이라고 하자.
위 조건에서 시뮬레이션을 초기화 할 경우 프로세스는 10개의 스레드를 생성하고 각 10개의 스레드는 개별적으로 Action Class 묶음 클래스를 생성한다. 즉, 10개의 Action Class 묶음이 만들어진다. 또 10개의 스레드를 관리하는 마스터 스레드를 추가로 생성하는데 마스터 스레드는 (사용자 설정이 가능하지만) 보통 Run Action 클래스만 생성하도록 한다. 굳이 Run Action 클래스를 생성하는 이유는 시뮬레이션이 시작하기 전, 혹은 모두 끝났을 때 종합적인 기능을 넣는 경우가 많기 때문이다. 런을 실행하게 되면 마스터는 스레드는 하위 스레드가 준비되는 대로 시뮬레이션을 수행하라는 명령을 내린다. 명령의 순서는 정할 수 없으며 하나의 스레드가 연속적으로 배정받을 수도 있다. 따라서 순서 의존적인 시뮬레이션을 할 수 없다. 이벤트를 수행 명령은 모든 스레드를 합쳐서 10000번이 된다.
주의 사항
Multi-threading을 고려한 시뮬레이션을 만들 때 가장 크게 고려해야 하는 것은 스레드 안정성(thread-safe)이다. 스레드가 완벽하게 독립할 수 있다면 가장 좋고 독립할 수 없다면 그 부분에 대한 안정성을 신경써야 한다. 스레드 안정성이란 모든 스레드가 공유된 기능을 사용하더라도 안정적으로 작동하는 것을 의미한다. 특히 Geant4는 입력과 출력이 이와 밀접한 관련이 있다.
입력의 경우 Primary Generator Action 에 쓰이는 이벤트 입력 파일이 있다면 매우 까다롭다. 이벤트 파일을 읽고 트랙 정보를 전달해 주는 클래스를 만들어야 하지만 그 클래스의 객체는 프로세스 안에서 유일해야하고 동시에 모든 스레드에서 접근해야 하기 때문에 스레드 안정성을 고려해야 한다. 또한 모든 스레드가 이 객체에 쉽게 접근할 수 있어야 한다. 이 부분은 위에서 언급한 마스터 스레드의 Run Action 클래스와 G4Mutex로 해결할 수 있다. 추후에 토픽에서 다루겠다.
출력을 위해서 스레드가 동시에 하나의 파일, 또는 하나의 데이터 구조에 접근하는 것은 매우 위험하다. 따라서 각 스레드에서 출력파일을 다른 이름으로 생성하고 각 파일에 데이터를 따로 쓰도록 하는 것이 바람직 하다. 다행이도 이 모든 기능이 구현된 툴이 바로 G4Tool 이다.
알아두자
- Geant4는 multi-threading 기능을 사용할 때 기본 설정으로 2개의 스레드를 사용하도록 되어 있다. 사용자는 스레드 개수를 G4MTRunManager::SetNumberOfThreads(G4int n) 함수로 설정할 수 있다.
- Geant4의 모든 example은 multi-thread를 염두해 둔 예제들이다.
- Geant4 설치를 DGEANT4_BUILD_MULTITHREADED 옵션을 추가하여 할 경우, 즉 multi-threading을 지원할 경우 매크로 상수 G4MULTITHREADED 가 정의 된다. 이를 이용하여 multi-threading 지원 여부에 의존적인 프로그래밍을 할 수 있다. 실제로 Geant4의 example은 이를 적극 활용한다.
- Sensitive Detector를 사용하는 경우 Detector Construction의 ConstructSDandField() 함수 안에서 선언하고 SetSensitiveDetector("이름",포인터) 함수를 사용하여 등록해야 한다.
- G4Allocator를 사용하는 경우 스레드 로컬이어야 하고 반드시 스레드 안에서 메모리를 할당해야 한다.
유용한 함수들
Multi-threading을 사용할 때 유용한 함수들을 적어둔다. (G4Threading는 네임스페이스다)
- G4Threading::G4GetNumberOfCores() : 사용할 수 있는 코어의 개수를 출력한다.
- G4Threading::G4GetThreadedId() : 스레드 번호를 출력한다.
- G4MTRunManager::GetMasterRunManager() : 마스터 런 메니저를 출력한다.
- G4MTRunManager::SetNumberOfThreads(G4int n) : 사용할 스레드의 개수를 설정한다.
- Action 클래스의 IsMaster() : 마스터 스레드의 Action 클래스인지 여부를 출력한다.
몇개의 스레드를 생성할 수 있을까?
최대로 생성할 수 있는 스레드 수는 컴퓨터에 따라서 다르다. 맥의 경우 터미널에서 다음 명령어로 알 수 있다.
얼마나 빠를까?
지금부터 만들어볼 프로그램으로 간단한 테스트를 해 본 결과 스레드 개수에 따른 프로그램의 경과 시간은 아래와 같다. 스레드의 개수와 정비례하지는 않는 것을 알 수 있다.
- 스레드 1개 : 4760 ms
- 스레드 2개 : 2502 ms (약 1.9 배 빠름)
- 스레드 4개 : 2285 ms (약 2.1 배 빠름)