21.4.2.1.1. 내부 `_task_id` 변수에 대한 동시 접근을 막기 위한 Mutex 및 스핀락(Spinlock) 적용

21.4.2.1.1. 내부 _task_id 변수에 대한 동시 접근을 막기 위한 Mutex 및 스핀락(Spinlock) 적용

앞선 21.4.2.1 단원에서 우리는 if (!is_running()) { start(); } 라는 순진한 방어 코드가 어떻게 Race Condition에 의해 무너지는지 참혹한 결말을 확인했다.

PX4 코어 메인테이너들은 이 문제를 해결하기 위해, ModuleBase 클래스의 뼛속 깊은 곳에 원자적(Atomic) 검증이라는 통곡의 벽을 세워두었다. 이 벽을 구성하는 핵심 부품은 _task_id라는 단일 정수형(Integer) 변수와, 이 변수를 보호하는 가벼운 자물쇠 체계(Locking Mechanism)이다.

1. 상태의 알파와 오메가: _task_id

is_running() 함수의 내부 구현을 까보면, 사실 별도의 화려한 boolean 플래그를 검사하는 것이 아니다. 오직 다음의 단 한 줄뿐이다.

// ModuleBase 내부 구현
bool is_running() const {
    return (_task_id.load() >= 0);
}

NuttX 커널이 스레드를 생성하면 반드시 0보다 큰 고유한 양수(PID)를 반환한다. 그리고 스레드가 죽으면 무조건 -1을 반환한다.
PX4 설계자들은 이 커널의 특성을 이용하여 _task_id 변수 하나만으로 1) 모듈이 살아있는지 여부, 그리고 2) 살아있다면 그 스레드를 찾아가서 목을 벨(kill) 수 있는 식별표라는 두 가지 역할(Two Birds with One Stone)을 완벽하게 해결해 버린 것이다.

2. 원자적 조작(Atomic Operation)의 마술: Compare-And-Swap (CAS)

그렇다면 여러 스레드가 동시에 task_spawn을 호출하여 덤벼들었을 때, ModuleBase는 어떻게 단 하나만의 모듈 생성을 허락하는 것일까?
이 비밀은 C++11 표준에 도입된 std::atomic 라이브러리의 무적기, compare_exchange_strong (일명 CAS: Compare-And-Swap) 에 숨어있다.

PX4의 모듈 시동 로직(ModuleBase::task_spawn)은 내부 깊숙한 곳에서 대략 이런 형태의 스핀락(Spinlock) 방어벽을 친다.

int task_id_expected = -1; // 나는 스레드가 꺼져(-1) 있을 것이라고 기대한다.

// 1. 메모리에 직접 락을 걸고 연산하는 Atomic CAS 블록
if (_task_id.compare_exchange_strong(task_id_expected, 0)) {
    // 2. 이 블록에 들어왔다는 것은:
    // (A) 방금 전까지 진짜로 _task_id가 -1 이었고,
    // (B) 내가 제일 먼저 메모리 인터럽트를 걸어서 그 안의 값을 0(Starting 상태)으로 뒤집었다는 뜻이다!
    
    // 이제 안심하고 진짜 스레드를 띄우고(task_spawn), 
    // 그 진짜 PID(양수)를 _task_id에 덮어쓴다.
    _task_id.store(real_pid);
    return 0; // 성공
} else {
    // 3. 간발의 차이로 내가 아닌 누군가가 먼저 CAS를 성공시켜 버려서,
    // 이미 _task_id가 -1이 아니라 0 이상으로 변해버렸다.
    PX4_WARN("Module is already initializing or running!");
    return -1; // 패배 인정, 즉각 생성 포기
}

2.1 무거운 Mutex 대신 가벼운 CAS 스핀락을 쓴 이유

만약 위 코드를 정통 POSIX Mutex(상호 배제 자물쇠)로 구현했다면, 운영체제(커널) 레벨까지 내려가서 줄을 서고 번호표를 뽑아 프로세스를 Sleep 상태로 전환하는 엄청난 컨텍스트 스위칭(Context Switching) 오버헤드가 발생했을 것이다.

하지만 std::atomic::compare_exchange_strong은 순수한 CPU 기계어 레벨(Hardware Architecture)에서 지원하는 Lock-free(락 프리) 명령어 집합이다.
이 명령어는 CPU가 메모리 버스(Memory Bus) 자체에 하드웨어적인 전기적 락을 1클럭(Clock) 동안 강제로 걸어버리고, “읽고, 비교하고, 쓰는(Read-Modify-Write)” 3단 과정을 다른 CPU 코어가 절대 범접할 수 없는 단 하나의 원자적(Atomic) 기계어 덩어리로 끝내버린다.

결과적으로 PX4 모듈은 가장 값싸고, 가장 빠르고, 가장 가벼운 기계어 자물쇠 하나만으로 수백 킬로헤르츠(kHz)의 동시 다발적 콜(Call) 공격을 우아하게 막아내는 통제력을 얻게 된 것이다.

이제 모듈이 중첩 생성을 회피하고 원자적으로 우아하게 태어나는 방어 체계를 확립했다. 다음 장(21.4.2.2)에서는 이렇게 확보된 통제권 아래에서, 모듈이 실질적으로 메모리(Heap)에 올라가서 활개를 치기 위한 start()instantiate()의 뼈대 로직을 구축해 보도록 하겠다.