# 멀티프로세스 유니코드 스캔 가이드
## 개요
EzTrans DLL을 사용한 유니코드 스캔에서 멀티프로세싱을 구현할 때 발견된 문제점과 해결책을 정리한 문서입니다.
## 왜 멀티프로세스인가? (Thread-Local 불가 이유)
### 테스트 결과 요약
| 단일 스레드 순차 | ✅ 작동 | 기본 사용법 |
| 단일 스레드 다중 엔진 | ✅ 작동 | 같은 스레드에서 번갈아 사용 가능 |
| Thread-Local 엔진 | ❌ 불가 | LoadLibrary가 동일 HMODULE 반환 |
| Mutex 보호 다중 엔진 | ❌ 일부 실패 | DLL 전역 상태 충돌 |
| 멀티스레드 동시 접근 | ❌ 크래시 | Segfault, Heap Corruption |
| **멀티프로세스** | ✅ **작동** | 유일한 병렬화 방법 |
### DLL 내부 구조 분석
```
┌─────────────────────────────────────────────────────────────┐
│ 단일 프로세스 내부 │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────┐ │
│ │ J2KEngine.dll 전역 상태 │ │
│ │ - 초기화 플래그 │ │
│ │ - 사전 데이터 │ │
│ │ - 내부 번역 버퍼 (Thread-Unsafe!) │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↑ ↑ ↑ │
│ │ │ │ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │Engine 1 │ │Engine 2 │ │Engine 3 │ │
│ │HMODULE: │ │HMODULE: │ │HMODULE: │ │
│ │0x648e0000│ │0x648e0000│ │0x648e0000│ ← 동일! │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ Thread 1 Thread 2 Thread 3 │
│ (번역 호출) (번역 호출) (번역 호출) │
│ ↓ ↓ ↓ │
│ └──────────────┼──────────────┘ │
│ ↓ │
│ 💥 버퍼 충돌! 💥 │
│ - 출력 혼합 │
│ - 빈 결과 반환 │
│ - Heap Corruption │
│ - Segmentation Fault │
└─────────────────────────────────────────────────────────────┘
```
### 핵심 발견사항
1. **LoadLibraryA 동작**: 이미 로드된 DLL에 대해 **동일한 HMODULE** 반환
```
Engine 1 HMODULE: 0x648e0000
Engine 2 HMODULE: 0x648e0000 ← 같은 핸들!
```
2. **DLL 전역 상태 공유**: Engine2를 초기화하지 않아도 Engine1의 초기화 상태로 번역 가능
```rust
engine1.initialize_ex("CSUSER123455", &dat_path)?;
engine2.translate_mmntw("テスト") ```
3. **동시 접근 시 충돌**: 번역 함수 내부 버퍼가 thread-safe하지 않음
- 같은 스레드에서 순차 호출: ✅ 작동
- 다른 스레드에서 동시 호출: ❌ 충돌
### 멀티프로세스가 유일한 해결책인 이유
```
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Process 1 │ │ Process 2 │ │ Process 3 │
├──────────────┤ ├──────────────┤ ├──────────────┤
│ J2KEngine.dll│ │ J2KEngine.dll│ │ J2KEngine.dll│
│ (독립 인스턴스)│ │ (독립 인스턴스)│ │ (독립 인스턴스)│
│ │ │ │ │ │
│ 전역 상태 A │ │ 전역 상태 B │ │ 전역 상태 C │
│ 버퍼 A │ │ 버퍼 B │ │ 버퍼 C │
└──────────────┘ └──────────────┘ └──────────────┘
↓ ↓ ↓
번역 결과 A 번역 결과 B 번역 결과 C
(정상) (정상) (정상)
```
각 프로세스는:
- **완전히 독립된 메모리 공간**
- **독립된 DLL 인스턴스**
- **독립된 전역 상태 및 버퍼**
---
## 핵심 문제점 및 해결책
### 1. Rust 테스트 바이너리의 특성
Rust의 `cargo test`로 생성된 바이너리를 자식 프로세스로 실행하면, **전체 테스트 하네스가 실행**됩니다.
```
# 부모에서 자식 스폰 시 발생하는 문제
자식 프로세스 stdout:
"running 3 tests"
"test test_foo ... ok"
...
```
**해결책**: 환경변수로 워커 모드를 체크하고, 특정 테스트만 `--exact` 플래그로 실행
```rust
// 워커 전용 테스트
#[test]
fn worker_runner() {
if let Ok(params) = env::var("WORKER_PARAMS") {
run_worker(¶ms);
std::process::exit(0); // 즉시 종료
}
}
// 코디네이터에서 스폰 시
Command::new(¤t_exe)
.env("WORKER_PARAMS", "...")
.arg("worker_runner") // 테스트 이름
.arg("--exact") // 정확히 이 테스트만
.arg("--nocapture") // stdout 캡처 안 함
.spawn()
```
### 2. stdout 버퍼 블로킹
여러 워커의 stdout을 **순차적으로 blocking read**하면, 첫 번째 워커가 완료될 때까지 다른 워커들의 버퍼가 가득 차서 데드락 발생.
**해결책**: 스레드와 채널을 사용한 병렬 읽기
```rust
use std::sync::mpsc;
use std::thread;
let (tx, rx) = mpsc::channel();
for worker in workers {
let tx_clone = tx.clone();
thread::spawn(move || {
let reader = BufReader::new(worker.stdout);
for line in reader.lines() {
tx_clone.send(line).ok();
}
});
}
drop(tx); // 원본 송신자 드롭
// 메인 스레드에서 모든 메시지 수신
for msg in rx {
// 처리
}
```
### 3. DLL 동시 로드 충돌
여러 프로세스가 동시에 같은 DLL을 로드하면 충돌 가능.
**해결책**: 워커 스폰 시 약간의 지연, 또는 워커 내에서 worker_id 기반 지연
```rust
// 코디네이터에서
for (worker_id, ...) in work_assignments {
spawn_worker(worker_id);
thread::sleep(Duration::from_millis(100)); // 스폰 간 지연
}
// 또는 워커에서
fn run_worker(worker_id: usize) {
thread::sleep(Duration::from_millis(worker_id as u64 * 500));
let engine = EzTransEngine::new(&dll_path)?;
// ...
}
```
### 4. 코디네이터에서 DLL 로드 후 크래시
코디네이터 프로세스에서 DLL을 로드/테스트한 후 워커를 스폰하면 ACCESS_VIOLATION 발생.
**해결책**: 코디네이터에서는 DLL을 로드하지 않음 (워커만 로드)
```rust
fn coordinator() {
// DLL 테스트 생략 - 워커들이 각자 로드
println!("Skipping DLL test in coordinator");
// 워커 스폰
for ... {
spawn_worker(...);
}
}
```
---
## 권장 아키텍처
```
┌─────────────────────────────────────────────────────────────┐
│ Coordinator Process │
│ - DLL 로드 안 함 │
│ - 워커 스폰 (순차적, 약간의 지연) │
│ - mpsc 채널로 모든 워커 메시지 수신 │
│ - 진행률 대시보드 표시 │
└─────────────────────────────────────────────────────────────┘
│ spawn │ spawn │ spawn
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Worker 0 │ │ Worker 1 │ │ Worker 2 │
│ - DLL 로드 │ │ - DLL 로드 │ │ - DLL 로드 │
│ - 작업 수행 │ │ - 작업 수행 │ │ - 작업 수행 │
│ - JSON 출력 │ │ - JSON 출력 │ │ - JSON 출력 │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
└────────────────┼────────────────┘
│ stdout (JSON)
▼
┌───────────────────────┐
│ Reader Threads │
│ (각 워커당 1개) │
└───────────────────────┘
│ mpsc channel
▼
┌───────────────────────┐
│ Main Loop │
│ - 메시지 처리 │
│ - 진행률 업데이트 │
│ - 결과 수집 │
└───────────────────────┘
```
---
## 워커-코디네이터 프로토콜
JSON 기반 메시지 프로토콜:
```rust
#[derive(Serialize, Deserialize)]
enum WorkerMessage {
Progress {
worker_id: usize,
current_code: u32,
tested: u32,
found: u32,
},
ChunkResult {
worker_id: usize,
problematic_chars: Vec<u32>,
},
Complete {
worker_id: usize,
total_tested: u32,
total_found: u32,
elapsed_secs: f64,
},
Error {
worker_id: usize,
message: String,
},
}
```
---
## 대안 접근법 비교
### ❌ Thread-Local Storage (TLS)
```rust
thread_local! {
static ENGINE: RefCell<Option<EzTransEngine>> = RefCell::new(None);
}
```
**불가 이유**: LoadLibrary가 동일 HMODULE 반환, DLL 전역 상태 공유
### ❌ Mutex 보호
```rust
let engine = Arc::new(Mutex::new(engine));
let guard = engine.lock().unwrap();
guard.translate_mmntw(text)?;
```
**문제점**: 병렬성 없음 (순차 실행과 동일), 일부 케이스에서 여전히 실패
### ❌ DLL 복사 후 다중 로드
```rust
// J2KEngine_1.dll, J2KEngine_2.dll 등으로 복사
let engine1 = EzTransEngine::new("J2KEngine_1.dll")?;
let engine2 = EzTransEngine::new("J2KEngine_2.dll")?;
```
**문제점**: 라이선스/리소스 문제 가능, 관리 복잡성 증가
### ✅ 멀티프로세스 (권장)
```rust
// 각 프로세스가 독립적으로 DLL 로드
fn worker_process() {
let engine = EzTransEngine::new(&dll_path)?;
engine.initialize_ex("CSUSER123455", &dat_path)?;
// 번역 수행...
}
```
**장점**:
- 완전한 격리
- 안정적인 병렬 처리
- 한 워커 크래시가 다른 워커에 영향 없음
---
## 실행 방법
```bash
# V2 (개선된 멀티프로세스) - 자동 프로세스 수
cargo test --target i686-pc-windows-msvc --test full_unicode_scan \
scan_entire_unicode_range_parallel_v2 -- --ignored --nocapture
# V2 - 8 프로세스 고정
cargo test --target i686-pc-windows-msvc --test full_unicode_scan \
scan_entire_unicode_range_parallel_v2_8_procs -- --ignored --nocapture
# 소규모 테스트 (100개 문자)
cargo test --target i686-pc-windows-msvc --test full_unicode_scan_mini \
test_mini_multiprocess -- --nocapture
# 중간 규모 테스트 (4000개 문자)
cargo test --target i686-pc-windows-msvc --test full_unicode_scan_mini \
test_medium_multiprocess -- --ignored --nocapture
```
---
## 성능 참고
| 단일 프로세스 | ~50-100 chars/sec | 기준선 |
| 4 프로세스 | ~200-400 chars/sec | 4배 향상 |
| 8 프로세스 | ~400-800 chars/sec | 8배 향상 |
- 전체 유니코드 (1,112,064 codepoints): 약 20-40분 예상 (8 프로세스 기준)
- CPU 코어 수에 따라 최적 프로세스 수 조정 권장
---
## 관련 파일
- `tests/full_unicode_scan.rs` - 전체 스캔 테스트 (V1, V2)
- `tests/full_unicode_scan_mini.rs` - 소규모/중간 규모 테스트
- `tests/multiprocess_test.rs` - 멀티프로세스 기본 패턴 테스트
- `tests/thread_safety_test.rs` - 스레드 안전성 테스트
- `tests/thread_local_test.rs` - Thread-Local 방식 테스트 (실패 케이스 기록)
---
## 테스트 재현 방법
Thread-Local 불가를 직접 확인하려면:
```bash
# 1. 단일 스레드 다중 엔진 (성공)
cargo test --target i686-pc-windows-msvc --test thread_local_test \
test_single_thread_two_engines -- --ignored --nocapture
# 2. DLL 핸들 동일성 확인 (동일 HMODULE 반환 확인)
cargo test --target i686-pc-windows-msvc --test thread_local_test \
test_dll_handle_identity -- --ignored --nocapture
# 3. 멀티스레드 동시 접근 (크래시 발생)
cargo test --target i686-pc-windows-msvc --test thread_local_test \
test_thread_local_separate_engines -- --ignored --nocapture
```