임베디드 개발에서 여러 작업을 동시에 다루는 방법은 오랫동안 실시간 운영체제(RTOS)의 몫이었다. 그런데 최근 Rust 진영에서는 async/await 문법을 마이크로컨트롤러까지 끌어온 Embassy 같은 프레임워크가 주목받고 있다. 네덜란드의 임베디드 컨설팅 회사 Tweede Golf는 STM32F446ZET6(180MHz) 보드에서 동일한 동작을 하는 프로그램을 Embassy/Rust와 FreeRTOS/C로 각각 구현해, 인터럽트 지연·프로그램 크기·RAM 사용량·개발 편의성이라는 네 가지 기준으로 비교했다. 저자는 스스로 Rust 쪽에 치우쳐 있음을 인정하면서도, 언어 자체의 우열이 아니라 '태스크를 다루는 두 모델'의 차이를 드러내는 데 초점을 맞췄다.
두 가지 동시성 모델
먼저 개념을 정리할 필요가 있다. Rust의 async 함수는 사실상 future를 반환하는 함수에 대한 문법 설탕이며, 컴파일러가 이를 상태 기계 객체로 변환한다. 이 상태 기계는 await 지점을 넘나들며 유지되어야 하는 변수만 저장하고, poll이 호출될 때마다 멈췄던 곳에서 재개된다. future는 게으르게(lazy) 동작해 poll될 때만 실행되며, 매번 무작정 poll하는 대신 waker가 '다시 확인하라'는 신호를 executor에 보내는 방식으로 효율을 확보한다. Embassy는 여기에 페리페럴용 비동기 인터페이스를 얹어, 예컨대 핀의 상승 에지를 기다리는 future가 첫 poll에서 자신의 waker를 전역 EXTI 배열에 등록하고, 해당 인터럽트가 발생하면 알맞은 태스크만 깨우도록 만들었다.
반면 RTOS는 모든 것을 독립된 스레드로 나눈다. 스레드는 상태 기계가 아니라 평범한 코드를 그대로 실행하기 때문에 특별한 방식으로 짜지 않아도 되지만, 스레드를 전환할 때 프로세서 컨텍스트 전체를 저장하고 복원해야 한다. 이 구조는 선점형(pre-emptive) 스케줄링에 적합해, 커널이 우선순위에 따라 공정하게 실행 시간을 배분하고 이벤트에 예측 가능한 시간 안에 반응할 수 있다. 중요한 차이는 Embassy가 선점을 하지 않는다는 점이다. 실행 중인 태스크는 스스로 무언가를 await할 때만 다른 태스크로 넘어가는 협력형(cooperative) 멀티태스킹이다.
같은 프로그램, 다른 구현
비교를 위해 저자는 지나치게 단순하지 않으면서도 현실에 근접한 세 가지 작업을 정의했다. 버튼 입력을 인터럽트로 처리하고, 주기적으로 LED를 다루며, UART로 메시지를 출력하는 구성이다. 버튼 인터럽트는 그 안에서 모든 처리를 하지 않고 executor 또는 스레드에 '발생했다'는 사실만 알린다. Embassy는 이 동작을 이미 내장하고 있어 별도 코드가 필요 없었지만, C에서는 인터럽트 함수를 직접 만들어 스레드에 통지해야 했다. 메시지 전달에는 용량 8개짜리 큐를 사용했고, Rust는 ArrayVec 라이브러리의 스택 할당 문자열을 쓴 반면 C는 간단한 타입을 손수 정의해 메모리와 크기를 직접 관리해야 했다.
측정의 정밀도를 위해 버튼 디바운싱은 일부러 넣지 않았다. 지연을 삽입하면 측정값이 왜곡되기 때문인데, 그 대가로 버튼 눌림 상태 변수는 다소 신뢰할 수 없게 되었다. 또한 Rust에서 send 결과를 unwrap하면 실질적 정보 없이도 포매팅 코드가 크게 부풀어 오르는 것을 발견해, 단순 panic으로 대체했다. 이런 세부 조정은 '가장 최적화된 답'이 아니라 '평범한 프로그램'을 만드는 것이 목표였음을 보여준다.
측정 결과와 실무적 함의
저자는 버튼을 100번 눌러 200개의 표본을 얻고 평균과 표준편차를 기록했으며, 다른 날 측정해도 결과가 재현되었다고 밝혔다. 결론적으로 Embassy/Rust는 인터럽트 지연, 프로그램 크기, RAM 사용량 등 사실상 모든 항목에서 앞섰다. 다만 FreeRTOS/C가 이긴 지점이 하나 있는데, 인터럽트 종료 시점부터 스레드가 깨어나기 시작하는 시점까지, 즉 순수한 컨텍스트 전환과 재개 시간만 떼어 보면 RTOS가 더 빨랐다. 문제는 FreeRTOS 쪽의 인터럽트 자체가 두 배가량 오래 걸린다는 데 있다. 그 원인이 STM Cube HAL 때문인지, FreeRTOS의 비효율인지, 아니면 스레드 신호 방식 자체에 내재한 것인지는 이 실험만으로 단정할 수 없으며, 여러 RTOS와 HAL을 더 시험해야 답할 수 있는 문제로 남겼다.
개발 편의성 측면에서 저자는 Embassy의 손을 들었다. 원래라면 인터럽트로 다뤘을 일을 그냥 await할 수 있다는 발상에 익숙해지면 코드가 매끄럽게 읽히고, 스레드를 직접 관리하는 번거로움이 사라진다는 것이다. Embassy가 인터럽트를 중심으로 설계된 덕에 통합감이 좋은 반면, FreeRTOS에서 인터럽트를 다루는 경험은 훨씬 덜 매끄러웠다. 다만 이 지점에는 뚜렷한 트레이드오프가 있다. RTOS 코드에서 가장 큰 개선책은 버튼 로직을 인터럽트 안으로 옮기는 것인데, Embassy는 인터럽트 함수를 프레임워크가 직접 생성하기 때문에 이런 자유가 허용되지 않는다. 즉 FreeRTOS/C의 자유도와 Embassy/Rust의 개발 편의성이 맞바꿈 관계에 있다.
마지막으로 실시간성에 대한 우려가 남는다. 협력형 스케줄링인 async 태스크는 선점할 수 없어, 하나의 executor 안에서는 더 급한 작업을 제때 실행하지 못할 수 있다. Embassy는 이를 인터럽트 컨텍스트에서 도는 추가 executor로 해결한다. waker가 태스크를 깨울 뿐 아니라, 인터럽트 위에 얹힌 executor라면 해당 인터럽트를 pending 상태로 만들어, 우선순위가 높은 executor가 낮은 쪽을 선점하도록 하는 구조다. 이 실험은 단일 보드의 특정 조합을 다룬 것이라 일반화에는 신중해야 하지만, 임베디드에서 async 모델이 성능과 생산성 양쪽에서 충분히 경쟁력 있는 선택지가 되었음을 구체적인 수치로 보여준다는 점에서 의미가 있다.