Electron Internals: Message Loop Integration
이 글은 Electron의 내부 구조를 설명하는 시리즈의 첫 번째 포스트이다. 이 글에서는 Node의 이벤트 루프가 Chromium과 어떻게 통합되는지 소개한다.
GUI 프로그래밍을 위해 Node를 사용하려는 여러 시도가 있었다. 예를 들어 GTK+ 바인딩을 위한 node-gui와 QT 바인딩을 위한 node-qt가 있다. 하지만 이들은 프로덕션 환경에서 작동하지 않았다. GUI 툴킷은 자체 메시지 루프를 가지고 있는 반면, Node는 libuv를 사용해 자체 이벤트 루프를 돌리기 때문이다. 메인 스레드는 동시에 하나의 루프만 실행할 수 있다. 따라서 Node에서 GUI 메시지 루프를 실행하는 일반적인 방법은 매우 짧은 간격으로 타이머를 사용해 메시지 루프를 돌리는 것이다. 이 방법은 GUI 인터페이스의 반응 속도를 느리게 만들고 CPU 리소스를 많이 차지한다.
Electron을 개발하는 과정에서 우리는 비슷한 문제에 직면했다. 단, 반대 상황이었다: Node의 이벤트 루프를 Chromium의 메시지 루프에 통합해야 했다.
메인 프로세스와 렌더러 프로세스
메시지 루프 통합에 대한 자세한 내용을 살펴보기 전에, 먼저 Chromium의 멀티 프로세스 아키텍처에 대해 설명한다.
Electron에는 두 가지 타입의 프로세스가 있다: 메인 프로세스와 렌더러 프로세스(이 설명은 상당히 단순화된 것이다. 전체적인 그림을 보려면 멀티 프로세스 아키텍처를 참고한다). 메인 프로세스는 윈도우 생성과 같은 GUI 작업을 담당한다. 반면 렌더러 프로세스는 웹 페이지 실행과 렌더링만 처리한다.
Electron은 JavaScript를 사용해 메인 프로세스와 렌더러 프로세스를 모두 제어할 수 있다. 이는 두 프로세스 모두에 Node를 통합해야 함을 의미한다.
Chromium의 메시지 루프를 libuv로 교체하기
첫 번째 시도는 Chromium의 메시지 루프를 libuv로 재구현하는 것이었다.
렌더러 프로세스는 비교적 쉬웠다. 렌더러 프로세스의 메시지 루프는 파일 디스크립터와 타이머만 감시하기 때문에, libuv와의 인터페이스만 구현하면 됐다.
그러나 메인 프로세스는 훨씬 더 어려웠다. 각 플랫폼마다 고유한 GUI 메시지 루프가 존재한다. macOS의 Chromium은 NSRunLoop
를 사용하고, Linux는 glib를 사용한다. 나는 네이티브 GUI 메시지 루프에서 기본 파일 디스크립터를 추출해 libuv에 전달하는 다양한 방법을 시도했지만, 여전히 작동하지 않는 예외 상황이 발생했다.
결국 나는 작은 간격으로 GUI 메시지 루프를 폴링하는 타이머를 추가했다. 이로 인해 프로세스가 일정한 CPU 사용량을 유지하게 되었고, 특정 작업에서 긴 지연이 발생했다.
별도의 스레드에서 Node의 이벤트 루프 폴링하기
libuv가 성숙해지면서 새로운 접근 방식이 가능해졌다. libuv는 이벤트 루프를 위해 폴링하는 파일 디스크립터(또는 핸들)인 backend fd 개념을 도입했다. 따라서 backend fd를 폴링하면 libuv에 새로운 이벤트가 발생했을 때 알림을 받을 수 있다.
Electron에서 나는 별도의 스레드를 생성해 backend fd를 폴링하도록 했다. libuv API 대신 시스템 호출을 사용해 폴링했기 때문에 이 방식은 스레드 안전했다. libuv의 이벤트 루프에 새로운 이벤트가 발생할 때마다 Chromium의 메시지 루프에 메시지를 전송했고, libuv의 이벤트는 메인 스레드에서 처리됐다.
이 방식을 통해 Chromium과 Node에 패치를 적용하지 않아도 됐고, 동일한 코드를 메인 프로세스와 렌더러 프로세스 모두에서 사용할 수 있었다.
코드 구현
메시지 루프 통합 구현은 electron/atom/common/
디렉토리 내 node_bindings
파일에서 확인할 수 있다. Node를 통합하려는 프로젝트에서 쉽게 재사용할 수 있다.
업데이트: 구현이 electron/shell/common/node_bindings.cc
로 이동되었다.