21.4.2.2.2. stop(): request_stop() 호출 이후 워커 스레드가 안전하게 종료될 때까지 대기하는 조건 변수(Condition Variable) 사용법
메모리를 성대하게 할당받아 태어난 모듈도 언젠가는 죽어야 한다. 터미널에서 사용자가 custom_app stop을 입력하면, ModuleBase는 자식 클래스가 정성껏 구현해 둔 stop() 메서드를 호출한다.
이 stop() 함수의 제1임무는 백그라운드에서 신나게 돌고 있는 워커 스레드(Worker Thread)의 무한 루프를 깨트리는 것이다. 하지만 이 과정은 단순히 변수 하나를 false로 바꾸고 남몰래 도망치는 뺑소니가 되어서는 안 된다.
모듈 종료 프로세스의 황금률(Golden Rule)은 **“내가 꽂은 종료 깃발을 보고, 런루프 스레드가 스스로 짐을 싸서 메모리에서 완전히 퇴장하는 꼴을 끝까지 지켜본 뒤에야 부모 함수의 실행을 마친다”**이다.
1. 깨어질 무한 루프: request_stop()과 should_exit()
PX4 모듈의 Run() 메서드 내부는 보통 다음과 같은 무한 루프 뼈대를 지닌다.
void CustomApp::Run()
{
// 센서 구독 등 초기화
// 무한 루프의 조건식: "시스템이 나더러 죽으라고(exit) 했는가?"
while (!should_exit()) {
// 실제 연산
}
// 루프를 탈출했다면 스레드가 종료되기 직전이다!
// 열어둔 포트나 메모리를 정리(Cleanup)한다.
}
이때 밖에서 누군가가 저 should_exit() 검사식을 true로 만들어 주어야 루프가 깨질 것이다. 그 역할을 하는 것이 바로 ModuleBase의 내장 함수인 **request_stop()**이다.
따라서 1차적인 stop() 함수의 모습은 이렇게 생겼다.
int CustomApp::stop()
{
// 워커 스레드 런루프의 while 조건식을 폭파시킴 (깃발 꽂기)
request_stop();
return 0;
}
2. 비동기 종료의 덫과 조건 변수(Condition Variable)
우위의 stop() 코드에는 치명적인 결함이 있다. request_stop()은 그저 메모리의 불리언(Boolean) 값을 true로 뒤집어 줄 뿐, 워커 스레드가 저 플래그를 읽고 루프를 빠져나올 때까지 기다려주지 않는다(Non-Blocking).
만약 워커 스레드가 하필 1초짜리 sleep() 에 빠져 있었거나 무거운 I/O 블로킹에 걸려 있었다면?
stop() 함수는 이미 return 0; 을 외치고 끝났는데, 워커 스레드는 아직 살아서 1초 뒤에 깨어나 할당 해제된 객체 포인터를 만지작거리려 할 것이다. 이것이 바로 악명 높은 Use-After-Free (메모리 해제 후 참조) 크래시다.
이 비동기 사각지대를 막기 위해 동기화의 끝판왕 격인 **조건 변수(Condition Variable)**가 등장한다.
2.1 stop() 함수의 완성형 패턴
올바른 stop() 함수는 무식한 sleep 대기가 아니라, 운영체제의 스케줄러를 활용해 스레드가 죽을 때까지 아주 효율적으로 잠들어서 기다려야(Wait) 한다.
int CustomApp::stop()
{
// 1. 죽으라는 깃발을 꽂는다.
request_stop();
// 2. 맹목적인 대기 대신, 상태 체크 폴링(Polling) 루프를 돈다.
// 최대 10번(1초)까지만 기다려본다 (무한 블로킹 방지)
int i = 0;
while (is_running() && i < 10) {
// 조건 변수 대기: 워커 스레드가 짐을 쌀 시간을 10만 마이크로초(0.1초) 씩 준다.
px4_usleep(100000);
i++;
}
// 3. 최후통첩: 1초가 넘었는데도 안 죽었는가?
if (i >= 10) {
PX4_ERR("Module failed to stop!");
return -1; // 강제 종료 불가 에러 반환
}
// 4. 모듈 객체를 힙에서 완전히 날려버린다 (자살 방지, 타살 허용)
// ModuleBase의 싱글톤 객체 포인터도 nullptr로 초기화된다.
// ... 내부적으로 cleanup 로직 호출 ...
return 0;
}
위의 폴링 루프에서 is_running()이 false가 되는 시점이 바로, 워커 스레드가 드디어 런루프를 탈출하고 자기 스레드의 _task_id를 -1로 강등시킨 바로 그 찰나의 순간이다.
이로써 터미널에서 stop을 외친 스레드와 백그라운드에서 돌던 워커 스레드가 메모리 충돌 없이 완벽한 타이밍에 악수하고 헤어지는(Handshake Teardown) 동기화가 달성된다.
생명주기를 마음대로 껐다 킬 수 있게 되었다. 다음 단원 21.4.2.2.3 에서는 살아서 펄떡거리는 모듈의 건강 상태를 터미널 창(status)으로 이쁘게 포맷팅(Formatting)하여 출력하는 기술을 살펴보자.