21.5.3.1.2. 파라미터 업데이트 발생 시 `updateParams()`를 호출하여 클래스 멤버 변수를 락-프리(Lock-free) 방식으로 동기화하는 기법

21.5.3.1.2. 파라미터 업데이트 발생 시 updateParams()를 호출하여 클래스 멤버 변수를 락-프리(Lock-free) 방식으로 동기화하는 기법

_parameter_update_sub.updated()가 드디어 true를 반환했다면, 런루프는 이제 즐거운 마음으로 파라미터를 최신값으로 갈아 끼우는 함수, **updateParams()**를 호출하게 된다.

이 함수 내부에서는 21.5.2 단원에서 우리가 선언해 두었던 수많은 ParamInt, ParamFloat 객체들이 일제히 C API(param_get)를 통해 새로운 값을 빨아들여 C++ 래퍼(Wrapper)의 멤버 변수로 밀어 넣는 동기화 작업이 연쇄적으로 일어난다.

하지만 여기서 멀티 코어(Multi-Core) 프로그래밍의 가장 끔찍한 악몽인 **경쟁 상태(Race Condition)**의 그림자가 어른거린다.

1. 동시성 제어의 공포: 데이터 찢어짐(Data Tearing)

만약 픽스호크 보드 내부에서,

  1. 백그라운원의 uORB 통신 스레드가 새로운 QGC 파라미터 값(15.0f)을 메모리에 한창 쓰고 있는 그 찰나의 순간(수 나노초)에,
  2. 하필 내 제어 모듈이 런루프 스레드에서 updateParams()를 외치며 그 메모리 위로 포인터를 들이밀어 값을 읽어 버린다면?

값이 절반만 쓰인 상태(예: 상위 2바이트만 갱신되고 하위 2바이트는 옛날 값인 상태)를 읽어버리는 끔찍한 데이터 찢어짐(Data Tearing) 현상이 발생하게 된다.
이런 더미 데이터를 들고 제어 로직을 돌리면 계산 결과가 극단적인 NaN(Not a Number)으로 폭주하고 만다.

2. 전통적인 Mutex 락(Lock)의 한계

일반적인 데스크톱 프로그래머라면 이 문제를 해결하기 위해 양쪽 스레드에 **Mutex(상호 배제 자물쇠)**를 걸어버렸을 것이다.

// 성능을 깎아 먹는 자물쇠 남용의 예
pthread_mutex_lock(&param_lock);
updateParams();  // 다른 스레드가 쓰고 있으면 내가 멈춰서(Sleep) 대기함!
pthread_mutex_unlock(&param_lock);

하지만 uORB 네트워크는 초당 수만 개의 메시지가 오가는 미친듯한 트래픽의 고속도로이다. 파라미터 동기화 하나 하겠다고 운영체제의 무거운 Mutex 자물쇠 채우기를 시도했다가 내 런루프가 3밀리초(ms)라도 블로킹(Blocking) 당해 잠들어 버린다면, 그 3밀리초 동안 처리되지 못한 센서 데이터들이 큐에 쌓여 결국 제어 지연(Latency)을 유발하게 된다.

3. Lock-free 동기화: 원자적 연산(Atomic Operation)의 미학

이 딜레마를 해결하기 위해, PX4의 파라미터 시스템(내부의 param_t 핸들 관리자)은 Mutex를 쓰지 않고 하드웨어 아키텍처 레벨(ARM Cortex-M)의 원자적(Atomic) 연산에 기대어 데이터를 동기화한다.

픽스호크의 심장인 ARM 프로세서에는 메모리의 4바이트(32bit)를 단 1 CPU 클럭(Clock)만에 일도양단으로 읽고 써버리는 기계어 수준의 원자적 명령어가 내장되어 있다. (과거에는 LDREX/STREX 류의 명령어들이 쓰였다.)

따라서 ParamFloat 객체가 param_get()을 통해 자신의 내부에 값을 덮어쓸 때, 이 작업은 32비트(Float 크기) 기준에서 절대로 찢어질 수 없는 하나의 완벽한 덩어리(Atomic) 단위로 처리된다.

결과적으로 내 런루프 스레드가 아무리 미친 속도로 updateParams()를 난사하더라도, 결코 uORB 스레드가 값을 쓰는 과정과 충돌하지 않게 된다.
두 스레드는 서로를 멈추게(Sleep) 하거나 자물쇠를 걸지 않고 번개처럼 순수한 락-프리(Lock-free) 포인터 연산만으로 최신 데이터를 안전하게 교환하는 것이다.


[ 21.5.장 요약 ]
우리는 거대한 ModuleParams를 상속받아 펌웨어와 파라미터 데이터베이스 간의 강력한 결속 고리(Binding)를 탄생시켰다. 이로써 런루프는 락-프리 비트 마스킹을 무기 삼아 단 1클럭의 낭비도 없이, 외부에서 날아오는 동적 변경 사항을 받아들일 수 있게 되었다.

이제 모듈 내부의 혈관과 근육(변수) 조립을 마쳤으니, 이 근육을 어떻게 운영체제의 스케줄러(Scheduler)라는 거대한 쳇바퀴 위에 올려서 가장 효율적으로 달리게 만들 것인가를 다루는 다음 챕터 21.6장(Work Queue 스레드 풀)으로 넘어가 보자.