티스토리 뷰

반응형

[※ 주의 ※] 아래를 이해하지 않고 이 글을 볼 경우, 이해가 되지 않는 부분이 있을 수 있습니다.

 

1.

2023.01.08 - [Javascript] - [JS / ECMAScript] 비동기 처리를 조금 더 효율적으로 해보자.


자바스크립트는 싱글 쓰레드 언어이다. 따라서, 쓰레드를 새로 만든다건가, 이벤트를 처리할 때 한 번에 단 한번의 이벤트만 처리할 수 있다. 그러나, 현대 언어는 이벤트 핸들링을 위하여 여러 가지 방법 및 디자인 패턴을 발전시켰고, 이를 알기 위해서는 여러 사전지식을 갖추어야 한다. Node.js나 Dart / Flutter에서 사용하는 event loop, 파이썬에서 활용하는 Global Interpreter Lock 등도 여러 배경 속에서 탄생한 이벤트 처리 방법이다.

 

이벤트 처리 문제를 알기 전, 먼저 두 가지 중요한 디자인 패턴에 대하여 알아보자.


1. 이벤트 핸들링 = 동시성 문제

이벤트를 처리하거나 핸들링 해야 한다는 것은 결국 동시성 문제와 연결된다. OS의 기저에 깔린 철학 중 하나가 Event-driven handling이기 때문에, 다른 일을 하고 있는 OS가 이벤트를 만나면 기존에 진행중인 작업과 새로 들어온 작업(이벤트)를 어떻게 처리해야 하는 지 정해야 한다.

 

기존의 언어들은 멀티쓰레드를 활용하고, C 또한 이벤트 핸들링을 보통 새로운 쓰레드를 만들어 해결한다. 그러나, 멀티쓰레드는 기본적으로 동기화 문제가 까다롭다는 단점도 있고, 이를 위하여 뮤텍스, 세마포어 등 여러 자료구조를 활용하지만 CPU의 자원을 많이 먹기에 CPU-bound 작업을 하면 안 쓰느니만 못할 수 있다는 단점이 존재한다.

 

이를 위하여 여러 현대 스크립트 언어는 다양한 방법을 차용하였다.

파이썬의 GIL 동작 원리 1
파이썬의 GIL 동작 원리 2

먼저, Python의 경우 전역 인터프리터 잠금(GIL)을 도입하였다. 파이썬의 경우도 여러 개의 쓰레드를 만들 수 있다. 다만, 기존의 멀티쓰레드 방식과 다르게 쓰레드 자체에 binary semaphore(mutex 아님)를 도입하여, 하나의 연속된 시간 속에 하나의 쓰레드만 실행되게 작동하도록 정하였다. 마치 코루틴을 실행하는 것처럼 말이다. 만약 쓰레드 하나가 I/O 작업을 커널에 요청하거나, CPU-intensive 작업을 해야하거나, timer interrupt가 발생한다면 GIL을 반납하고, 다른 쓰레드가 실행되도록 한다.

 

이러한 작업은 I/O 작업에는 여러 이득을 볼 수 있다. 다만, CPU-bound 작업을 처리하면 오히려 느려진다는 단점이 있고, 멀티프로세스보다 멀티쓰레드가 더 느려지는 경우가 여럿 발생하였다. 이러한 문제는 C 라이브러리들과의 바인딩을 통해 해결한다. 다만, 근본적인 해결책은 되지 못한다.

 

따라서, 최근에는 Python에서 GIL을 없애고 이와 관련된 코드를 제거하자는 목소리가 높아지기 시작했다. GIL이 도입된 이유가 garbage collection이기도 하여 GC의 알고리즘을 수정하고, thread-safe하게 함수를 작성하여 Python을 더욱 빠르게 하자는 이야기가 나왔다.

 

파이썬 3.10부터 GIL을 차츰 버릴 준비를 하고있으며, 속도 향상에 매력을 느낀 파이썬 코어 개발자들은 서서히 GIL에 관한 업데이트를 진행중인 분위기이다.

 

Ruby의 Guild Model
Ruby의 멀티쓰레드

이러한 GIL의 부작용을 보며, GIL의 장점과 기존 멀티쓰레드의 장점을 합칠 방법을 생각하기 시작했다. 한 가지 예시로 Ruby는 Ruby3가 업데이트되며 Ractor(Guild)라는 개념을 도입하여, 부분적인 멀티쓰레드를 도입하기 시작하였다. 해결책은 객체를 공유하는 쓰레드끼리는 lock을 걸되, 다른 쓰레드는 해당 객체에 접근하지 못하는 대신 쓰레드를 동시에 실행시킬 수 있는 방법을 제시하였다. 객체를 공유할 수 있는 쓰레드의 묶음을 Guild라고 부르기로 하였고, 각 Guild에서는 GGL(Giant Guild Lock)을 걸어 하나의 쓰레드만 실행시킬 수 있었다.  대신, 서로 다른 길드 안의 쓰레드는 병렬적으로 실행할 수 있도록 하였다.

각 길드간 통신은 message passing channel을 만들거나, 해당 객체의 reference만 공유하거나, 해당 객체의 guild 소유권을 바꾸는 방식을 사용하거나, 특별한 자료구조 buffer를 만드는 것으로 해결하였다. 


2.  Reactor & Proactor

다른 스크립트 언어, 특히 싱글쓰레드 언어들은 이벤트 핸들링을 위해 I/O 멀티플렉싱에서 아이디어를 따왔다. 이벤트 핸들링은 non-blocking 형태로 작동해야 하기에, non-blocking 하면서 synchronous한 방식인 reactor 패턴, asynchronous한 방식인 proactor 패턴 방식이 제시되었다. (Busy-waiting은 비효율적인 방법이기에 제외하였다.)

 

먼저, 이벤트 처리는 다음 다섯 단계로 나눌 수 있다.

 

  1. 이벤트가 발생하면 입력을 처리할 수 있도록 다중화한다. (Initiate & multiplex)
  2. 이벤트가 발생하기까지 대기한다. (Receive)
  3. 이벤트가 발생 시 event handler를 호출하기 위하여 객체로 분할한다. (demultiplex)
  4. 해당 이벤트에 해당되는 event handler를 호출한다. (dispatch)
  5. Event handling을 수행한다. (process)

Reactor 패턴 흐름도

Reactor 패턴은 동적으로 event handler를 생성하고 지우는 방법을 활용하여 여러 이벤트를 처리한다. 즉, 이벤트에 반응하는 객체(reactor)를 생성하고, 이벤트 발생 시 해당 이벤트 target 대신 reactor가 해당 이벤트를 처리한다. 따라서 reactor 패턴의 가장 큰 참여자는 reactor 자체와 이벤트에 따른 handler이다. 

 

따라서, 프로그램의 흐름이 역전되는것과 같은 효과를 주며, 해당 이벤트 처리에 대한 책임은 reactor에 있고, 어플레케이션을 설계하는 사람은 이벤트 handler에 대한 책임이 있다. 이러한 원칙을 "할리우드 원칙(Hollywood principle)"이다. 

Don't call us, we'll call you.
- Hollywood principle -

위의 5단계로 설명하면 다음과 같이 설명할 수 있다.

 

  1. 이벤트에 반응하는 Reactor 객체를 생성하고 이벤트에 알맞은 handler를 등록한다. (Initiate & multiplex)
  2. 이벤트가 발생하기까지 Reactor가 대기한다. (Receive)
  3. 이벤트 발생 시 Reactor는 이벤트 처리를 위해 handler 단위로 이벤트를 분리한다. (demultiplex)
  4. 분할된 이벤트에 따라 handler가 처리하도록 신호를 보낸다. (dispatch)
  5. handler에 따른 이벤트를 처리한다. (process)

 

Reactor 패턴을 활용하는 대표적인 예제는 Node.js나 Dart의 event loop이다. 이벤트가 발생하면 JS엔진 안의 reactor가 알맞게 event handler를 호출하며, 해당 이벤트 핸들링 콜백 함수를 CallbackQueue에 등록하는 방법으로 작동이 된다. 자세한 내용은 event loop에 대한 글에서 찾아뵙도록 하겠다.

Linux 안의 epoll도 reactor 패턴을 사용하며, 이 경우 fd마다 event handler를 만들어, 이벤트 발생 시 연결한 event handler에 등록된 fd와 이벤트 종류를 포함한 이벤트 구조체를 반환한다. 따라서, 이벤트와 handler에 대한 정보를 리액터가 관리해야 하며 수많은 요청을 여러 개의 handler로 처리하면 부담이 그만큼 커지게 된다.

또한, 멀티쓰레드의 경우 쓰레드 별 reactor를 사용해야 하는데, 쓰레드 별 reactor를 사용할 경우 비효율적인 I/O 처리를 하므로 직접 스케쥴링 정책을 만들어야 한다는 단점이 있다.

 

따라서, Reactor 패턴은 직관적이라는 장점이 있지만 여러 개의 client를 핸들링하거나, 멀티쓰레드로 동작하는 경우 적절하지 않는 패턴이다.

Proactor 패턴 흐름도

Proactor 패턴은 Reactor 패턴의 단점을 해결할 수 있다. Proactor는 반대로 이벤트를 수동적으로 기다리지 않고, event handling을 능동적으로(비동기적으로) 한다. OS가 비동기 작업 프로세스를 지원하는 경우, 특정 작업을 시키며 콜백 함수를 직접 넘겨주며 해당 일을 비동기적으로 처리하고 작업이 끝났는지 콜백을 받으면서 알게된다. 즉, proactor 패턴에서는 event가 하나의 작업 완료 콜백이라고 생각하면 된다.

 

위의 5단계로 설명하면 다음과 같이 설명할 수 있다.

 

  1. 완료 이벤트를 받을 completion handler를 등록한다. (Initiate & multiplex)
  2. 비동기 프로세스가 작업을 대기하거나 작업 발생 시 처리한다. (Receive)
  3. 가능한 작업 생길 시 비동기 프로세스가 작업을 알아서 분리하여 비동기 처리한다.(demultiplex)
  4. 작업 완료 시 비동기 프로세스는 작업이 완료되었다고 completion dispatcher에게 신호를 주며, dispatcher는 해당 이벤트에 대한 적절한 completion handler에게 넘긴다.
  5. completion handler는 사전에 정해진 콜백 함수를 호출하여 마무리한다. (process)

즉, 이벤트가 발생하면 어플리케이션은 OS의 비동기 작업 API에 이벤트를 던져주면 비동기적으로 작동하며 "알아서" 처리해서 통지하고, 이후 completion handler에 도착한 이벤트만 처리하면 된다. 마치 친구에게 부재중 전화를 남기면 친구가 부재중 전화를 보고 나에게 다시 전화를 걸어 통화하는 방식에 비유할 수 있겠다.

 

Proactor 패턴의 장점은 콜백에 따른 작업만 처리하면 되게에 간결하게 설계할 수 있다. 또한, 확장성이 좋고 이벤트 제어권이 어플리케이션에 있으므로 다른 동기화 작업이나 동시성 작업 처리가 쉬워진다. 다만, 비동기성을 활용하다 보니 작동이 어떻게 할지에 대하여는 무작위적이고 디버깅 / 테스트가 어렵다는 단점이 존재한다. 또한, 비동기 작업에 따른 오버헤드도 당연히 존재하고, 생각보다 퍼포먼스 발목을 잡는 부분이 많다.

 

보통 Proactor 패턴은 비동기 작업 쓰레드 혹은 쓰레드 풀을 만들고 비동기 쓰레드에 작업을 던져주는 방식으로 동작한다. C++ Boost 라이브러리의 Asio(Asynchronous IO)도 해당 방식을 활용하며, 뒷 단의 worker에서 비동기 I/O API를 활용하거나 직접 쓰레드가 비동기 작업을 처리한다.

 

이제, JS의 Event Loop을 이해하여보자.

 

3. Reference (감사합니다)

https://www.datacamp.com/tutorial/python-global-interpreter-lock

 

https://www.dabeaz.com/python/UnderstandingGIL.pdf

 

https://docs.google.com/document/d/18CXhDb1ygxg-YXNBJNzfzZsDFosB5e6BfnXLlejd9l0/edit

 

http://www.atdot.net/~ko1/activities/2016_rubykaigi.pdf

 

https://blog.naver.com/n_cloudplatform/222189669084

 

http://www.dre.vanderbilt.edu/~schmidt/POSA/POSA2/event-patterns.html

 

https://brunch.co.kr/@myner/42

 

http://didawiki.cli.di.unipi.it/lib/exe/fetch.php/magistraleinformatica/tdp/tpd_reactor_proactor.pdf

 

https://www.boost.org/doc/libs/1_72_0/doc/html/boost_asio/overview/core/async.html

반응형
댓글
Total
Today
Yesterday
공지사항
최근에 올라온 글
최근에 달린 댓글
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함