로봇 상태를 화면에 띄우는 프로그램은 구조가 정해져 있어요. 데이터를 계속 받아 오는 쪽과 그걸 그리는 쪽이 따로 돌죠. 받는 쪽을 화면과 같은 흐름에 두면 데이터를 기다리는 동안 창이 얼어붙기 때문이에요.
그래서 별도 흐름을 만드는데, 여기서 이 글의 규칙 하나가 나와요. 화면 객체를 만질 수 있는 흐름은 하나뿐이에요.
직접 고치면 당장은 돌아가요
화면 객체는 주 흐름의 소유예요. 다른 흐름이 동시에 만지면 내부 상태가 깨져요.
문제는 당장 안 죽는다는 거예요. 워커에서 라벨 글자를 바꾸는 코드를 쓰면 잘 돌아가요. 몇 분, 며칠도 돌아가죠. 그러다 무작위 시점에 죽어요. 두 흐름이 같은 자리를 정확히 같은 순간에 건드릴 때만 깨지는데, 그 확률이 낮을 뿐 0이 아니거든요.
이 종류의 버그가 고약한 건 재현이 안 된다는 점이에요. 죽은 자리를 봐도 원인이 안 보이고, 다시 돌리면 멀쩡하죠. 되는 걸 확인했다는 것이 안전하다는 근거가 안 되는 대표적인 자리예요.
신호를 쏘면 프레임워크가 배달해요
정석은 워커가 화면을 건드리지 않는 거예요. 대신 데이터를 신호로 내보내고, 주 흐름에 연결된 함수가 그걸 받아 화면을 고쳐요.
연결할 때 프레임워크가 양쪽 흐름이 다르다는 걸 알아채면 자동으로 큐 방식으로 전달해요. 내보낸 데이터는 주 흐름의 이벤트 큐에 쌓였다가, 주 흐름이 한가할 때 하나씩 꺼내져 함수가 불려요.
즉 이 구조는 생산자와 소비자를 큐로 잇는 그 패턴 그대로예요. 다른 점은 큐를 내가 안 만든다는 것뿐이고요.
| 직접 만든 큐 | 신호 방식 |
|---|---|
| 생산자가 큐에 넣음 | 워커가 신호를 내보냄 |
| 소비자가 큐에서 꺼냄 | 주 흐름의 함수가 받음 |
| 큐 자체가 안전하게 만들어짐 | 전달 방식이 안전하게 깔림 |
그래서 규칙이 단순해져요. 워커는 오직 내보내기만, 화면 접근은 전부 받는 함수 안에서만. 그 함수는 주 흐름에서 도니까 안전하거든요.
보내는 쪽에 속도 제한이 필요해요
워커 루프에 대기가 없으면 어떻게 될까요. CPU를 다 태우면서 큐를 폭주시켜요.
주 흐름은 화면을 그리느라 바쁜데 워커는 최대 속도로 데이터를 밀어 넣으니, 큐가 계속 자라요. 메모리를 먹다가 결국 응답이 느려지고요. 화면은 어차피 초당 몇십 번 이상 갱신해 봐야 사람 눈에 차이가 없으니, 보내는 쪽에서 주기를 정해 두는 것이 맞아요.
그만두라는 말은 부탁이에요
시작만큼 중요한 게 끝내기예요. 그리고 여기가 가장 자주 죽는 자리예요.
중단을 요청하는 함수는 즉시 죽이지 않아요. 그만 두라는 표시를 올릴 뿐이고, 워커 루프가 다음 바퀴에서 그 표시를 보고 스스로 끝내야 해요. 그래서 두 단계가 짝을 이뤄요.
| 단계 | 하는 일 |
|---|---|
| 중단 요청 | ”그만” 표시를 올림 — 비동기 부탁 |
| 루프의 확인 | 매 바퀴마다 그 표시를 봄 |
| 종료 대기 | 실제로 멈출 때까지 기다림 |
세 번째가 빠지면 사고가 나요. 창을 닫는 순간 객체들이 정리되기 시작하는데, 워커가 아직 돌면서 화면 객체를 참조하면 그 사이에서 죽어요.
증상이 특징적이에요. 프로그램은 잘 돌아갔고, 닫을 때만 죽어요. 그래서 “작업은 다 끝났는데 종료가 지저분하네” 정도로 넘기기 쉬운데, 실은 자원을 안 놓고 죽은 거예요. 시리얼 포트를 쥔 채로 죽으면 다음 실행에서 포트가 이미 사용 중이라고 나오죠.
창 닫힘을 처리하는 곳에서 워커를 정리하는 게 규칙이에요. 연결을 끊는 버튼도 같은 정리 함수를 부르고요. 다시 연결할 때는 워커를 새로 만드는데, 상태가 초기화되는 게 의도한 동작이에요.
주기 작업이 두 종류예요
둘 다 “주기적으로 뭔가 한다”인데 도는 흐름이 달라요.
별도 흐름의 워커는 오래 걸리거나 기다리는 일에 써요. 포트를 읽거나 기록을 재생하는 것들이요. 여기서 화면 접근은 금지예요.
주 흐름의 타이머는 이벤트 루프가 주기적으로 불러 줘요. 가볍고 화면 접근이 안전해요. 1초마다 지난 1초간 받은 프레임 수를 세어 표시하는 식으로 쓰죠.
이 수신율 표시가 생각보다 쓸모 있어요. 워커가 20Hz로 보내는데 표시가 20 근처로 찍히면 파이프라인이 건강하다는 뜻이고, 뚝 떨어지면 그리는 쪽이 병목이라는 신호예요. 3차원 렌더 같은 무거운 위젯을 붙였을 때 바로 드러나요.
숫자 하나가 어느 쪽이 느린지를 말해 준다는 게 핵심이에요. 화면이 버벅일 때 보내는 쪽을 볼지 그리는 쪽을 볼지가 이걸로 갈려요.
같은 규칙이 로봇 쪽에도 있어요
정리하면 이 영역의 규칙은 셋이에요. 화면은 한 흐름만 만진다, 보내는 쪽에 주기를 둔다, 끝낼 때는 요청하고 확인까지 한다.
세 번째가 특히 언어를 가리지 않아요. 비동기 요청과 실제 종료 확인을 짝으로 두는 것은 어느 환경에서나 같고, 안 지켰을 때의 증상도 같아요. 자원을 쥔 채로 죽는 거죠.
받는 쪽과 처리하는 쪽을 나누는 이야기는 C++ 동시성 — 센서 수신과 제어 루프에, 주기를 지키는 감각은 C++ 실시간 제약 — 핫루프와 링버퍼에 있어요.