Unitree_sdk2py와 파이썬 : 500Hz 제어 루프부터 멀티프로세싱까지

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 안에 맞추기 어렵기 때문입니다.

1개의 좋아요