1. 500Hz 제어 루프란?
Hz = 초당 반복 횟수
500Hz 제어 루프 = 1초에 500번 아래 사이클을 반복합니다.
[현재 관절 각도 읽기] → [목표 각도 계산] → [모터 명령 전송]
1회 사이클 간격 = 1 / 500 = 0.002초 (2ms)
빠를수록 로봇이 외란에 더 빠르게 반응할 수 있습니다. 사람의 신경계가 근육에 신호를 보내는 것과 같은 원리입니다.
2. 그럼 더 빠르게 할 수 없나? → 타이밍 지터 문제
더 빠르게 하면 타이밍 지터가 문제가 됩니다.
지터 = 실행 간격의 불규칙한 오차
이상: 0ms 2ms 4ms 6ms ... (정확히 2ms 간격)
실제: 0ms 2.3ms 3.8ms 6.1ms ... (들쭉날쭉)
지터가 크면 모터 명령 타이밍이 불규칙해져 진동이나 떨림이 발생합니다.
Python에서 지터가 생기는 원인이 바로 GIL 입니다.
3. GIL이란?
GIL = Global Interpreter Lock
Python 인터프리터 내부의 자물쇠로, 한 번에 하나의 스레드만 실행 가능하다는 규칙입니다.
Python은 메모리 관리를 위해 객체마다 참조 카운터를 씁니다. 여러 스레드가 동시에 이 카운터를 수정하면 메모리가 망가지기 때문에 GIL로 막아둔 것입니다.
스레드A (제어 루프) ──실행──┐
GIL ← 한 번에 하나만 통과
스레드B (GUI) ──대기──┘
제어 루프가 정확히 2ms에 깨어나야 하는데, 그 순간 GUI 스레드가 GIL을 잡고 있으면 기다려야 합니다. 이게 지터의 원인입니다.
4. 그럼 Python의 멀티스레드는 뭔가?
GIL 때문에 Python 멀티스레드는 "동시 실행"이 아니라 “빠른 교대 실행” 입니다.
스레드A ──실행──┐양보
스레드B └──실행──┐양보
스레드A └──실행──
그런데도 유용한 이유는, I/O 대기 중에는 GIL을 자동으로 놓아주기 때문입니다.
스레드A: 네트워크 수신 대기 중... (GIL 해제)
스레드B: 그 사이 실행! ← 진짜 병렬처럼 동작
이 프로젝트에서 멀티스레드가 효과적인 이유가 바로 이것입니다. 제어 루프의 DDS 전송, GUI 갱신, TTS 음성 전송 모두 I/O 위주라 교대 실행만으로도 충분합니다.
5. 그럼 멀티프로세싱을 쓰면 진짜 병렬이 되지 않나?
됩니다. 프로세스마다 Python 인터프리터가 별도로 실행되어 GIL을 각자 가지므로 진짜 병렬입니다.
프로세스A [GIL_A] CPU 코어1 ──실행──
프로세스B [GIL_B] CPU 코어2 ──실행── ← 진짜 동시 실행
하지만 항상 더 빠르다고 보장할 수는 없습니다.
| 단점 | 이유 |
|---|---|
| 프로세스 생성 비용 | 스레드보다 훨씬 무거움 |
| 데이터 전달 비용 | 메모리 공유 불가, pickle로 직렬화해서 복사해야 함 |
| 코어 수 한계 | 프로세스가 코어보다 많으면 오히려 경쟁 발생 |
작업이 짧거나 데이터를 자주 주고받으면 오히려 느려집니다.
정리
500Hz 제어 루프
└─ 더 빠르게? → 지터 문제
└─ 지터의 원인? → GIL
└─ GIL 때문에 멀티스레드가 무의미? → I/O에서는 유효
└─ 진짜 병렬하려면? → 멀티프로세싱
└─ 항상 빠른가? → NO, 비용 존재
G1 제어에서는 500Hz 멀티스레드 구조가 최적입니다.
DDS 전송이 I/O 위주이고, 프로세스 간 low_state 공유 비용이 2ms 안에 맞추기 어렵기 때문입니다.