21.7.3. Lock-Free 방식의 토픽 데이터 발행(Publication)
앞선 장들에서 우리는 데이터를 읽어가는 ’구독자(Subscriber)’의 입장에 서서 uORB 시스템을 찬양했다. 하지만 입장을 바꿔서 데이터를 쓰는 **‘출판자(Publisher)’**가 되어보면, 실시간 멀티스레드 환경에서 공유 메모리에 데이터를 쓴다는 것이 얼마나 소름 돋게 무서운 일인지 깨닫게 된다.
만약 자이로 센서 드라이버 스레드가 sensor_gyro 게시판에 방금 읽어온 새로운 [X, Y, Z] 구조체 데이터를 한참 덮어쓰고 있는 도중이라고 가정해 보자.
X 파라미터를 막 적어 넣고(10.5), Y 파라미터를 적으려는(20.3) 찰나에 갑자기 인터럽트가 걸리며 제어권이 자세 제어기 스레드로 넘어가 버렸다.
자세 제어기는 신나게 게시판으로 달려가 구조체를 복사해 간다. 하지만 그 데이터는 X=10.5(새 값), Y=0.0(아직 안 쓴 옛날 값), Z=0.0(옛날 값) 이라는, 앞뒤가 전혀 안 맞는 끔찍한 짬짜면 데이터(Torn Data, 데이터 찢김 현상)가 되어버린다. 기체는 이 괴상한 차원 왜곡 데이터에 얻어맞고 즉시 공중제비를 돌며 추락할 것이다.
1. 전통적인 해법: 뮤텍스 락(Mutex Lock)의 병목
일반적인 컴퓨터 공학에서 이 데이터 찢김 현상을 막는 가장 쉬운 방법은 병렬 컴퓨팅의 교과서인 **뮤텍스 락(Mutex Lock)**을 거는 것이다.
센서 스레드는 데이터를 쓰기 전에 자물쇠(Lock)를 채우고, 다 쓴 뒤에 푼다. 제어기 스레드는 자물쇠가 걸려있으면 스레드를 멈추고(Sleep/Blocked) 하염없이 자물쇠가 열리기만을 기다린다.
하지만 21.6.4장에서 피를 토하며 경고했듯, PX4의 Work Queue 시스템 안에서 누군가를 함부로 멈춰 세우는(Blocking) 행위는 곧 버스 전체의 죽음(Deadlock)을 의미한다. uORB 시스템 코어가 이런 구시대적인 뮤텍스 락을 썼다면 픽스호크는 100Hz조차 제대로 돌리지 못하고 헉헉댔을 것이다.
2. PX4 코어팀의 기적: Lock-Free 알고리즘
PX4 코어 아키텍처는 이 딜레마를 돌파하기 위해 스레드를 멈추지 않고도(Non-blocking) 완벽한 데이터 무결성을 보장하는 Lock-Free(락 프리) 공유 메모리 동기화 트릭을 구현해 냈다.
uORB 매니저가 데이터를 게시판에 덮어쓸 때, 전통적인 락을 거는 대신 아래와 같은 교묘한 하드웨어 레벨의 비동기 트릭을 쓴다.
- 복수 버퍼링 (Multi-level Buffering): uORB 토픽 내부는 단 하나의 칠판이 아니다. 보통 2~4개 정도의 동적 배열 메시지 큐(Message Queue)가 둥글게 링 버퍼(Ring Buffer) 형태로 존재한다.
- 원자적 스위칭 (Atomic Pointer Swap): 센서 스레드는 현재 구독자들이 읽고 있는 பழைய 버퍼를 건드리지 않는다. 아무도 안 보는 ’다음번 빈 버퍼’에 X, Y, Z 값을 여유롭게 다 쓴다.
- 다 쓴 다음, “자, 이제부터 진짜 최신 데이터는 이거다!” 라며 메인 포인터를 단 한 번의 커널 레벨 **메모리 원자적 연산(Atomic Instruction, 쪼개질 수 없는 하드웨어 명령)**으로 탁! 하고 스위칭해 버린다.
이 원자적 연산은 CPU 클럭 1사이클 만에 끊어지지 않고 완료되므로, 제어기 스레드 입장에서는 읽을 때마다 무조건 완벽하게 조립된 구버전 데이터나 신버전 데이터 중 하나만을 가져오게 된다. (데이터 찢김 원천 차단)
이처럼 락다운을 겪지 않는 출판 시스템의 최전선에는, C 언어 함수를 감싸서 메모리 릭(Leak)과 파일 기술자(File Descriptor) 고갈을 영구히 막아주는 최신 uORB::Publication<T> C++ 래퍼 클래스가 버티고 서 있다.
다음 장 21.7.3.1에서 이 래퍼의 광고(Advertisement) 초기화 과정을 낱낱이 파헤쳐보자.