Event Loop

이벤트 루프는 특정 이벤트나 메시지가 발생할 때까지 대기하다가 이벤트가 발생하면 디스패치하는 디자인 패턴이다. 각 운영체제에서도 각각의 이벤트 루프가 다양한 방식으로 활용되고 있으며, 자바스크립트의 경우는 싱글스레드 언어이기 때문에 node.js를 통해 하나의 호출스택만 사용하는 구조에서 동시성을 구현하기 위해 사용하는 디자인 패턴이다.

동시성: 서로 다른 사건이 동시에 일어나는 것

왜 나왔을까?

고전적으로는 이 동시성을 위해 멀티스레드를 열고 작업이 완료될 때까지 스레드의 실행을 차단하는 방법을 사용했다. 하지만 여기에선 쓰레드를 열었음에도 idle time, 즉 유효한 시간이 각각의 쓰레드에게 발생하기 때문에 이는 곧 자원의 낭비로 이어진다.

이어 나온 방식은 논블로킹의 이벤트 디멀티플렉싱메커니즘이다. 멀티플렉서가 요청들을 하나로 묶고 디멀티플렉서가 요청을 싱글 스레드에 나눔으로써 모든 요청들을 관리하고 요청이 완료되기 전까지 블로킹, 완료가 된 후에는 이벤트큐에 푸시하는 방법 또한 사용했다. 이러한 메커니즘에 특화된 디자인 패턴으로는 리액터 패턴이 있다.

리액터 패턴은 이벤트 디멀티플렉서에서 I/O 요청이 완료되었을 때, 이벤트와 이벤트에 해당하는 핸들러를 이벤트 큐에 넣고 이벤트 루프를 통해 이러한 핸들러를 실행시키는 방식으로 작동한다. 여기서 이벤트 루프가 존재를 드러내기 시작한다. 이러한 이벤트 루프의 기본적인 작동 방식이 현재 우리가 알고 있는 Node.js 등의 이벤트 루프의 기반으로 작동한다.

그럼 이러한 작동 방식에서 기본적인 구조인 Event Queue, Loop, Handler에 대해서 알아보자.

Event Queue

이벤트 큐는 메시지와 이벤트를 보내는 시점과 처리하는 시점을 다르게 하기 위한 비동기형 디자인패턴으로 관찰자 패턴의 형태이다. 이벤트가 계속해서 들어오면 이러한 이벤트를 처리하기 위해 프로그램은 이벤트 루프를 통해 이벤트 핸들러를 담은 이벤트를 실행한다.

while(running){
	Event event = getNextEvent();
}

수도코드를 보면 아직 처리하지 않은 사용자 입력을 가져오고 실행을 반복하느느 구조를 지닌다. 이러한 요청 안에는 클로저, 즉 해당 이벤트가 끝난 뒤 실행하는 함수인 이벤트 핸들러가 담겨져 있기 때문에 작업이 끝난 후 별도의 스레드에서 실행된다. 그렇기 때문에 이벤트루프는 이러한 작업을 핸들러에게 위임하고 바로 애플리케이션의 다음 IO를 받을 수 있도록 하는 비동기성을 가진다. 이를 통해 애플리케이션과의 상호작용이 가능한 것이다.

EventQueue에 있는 이벤트는 이벤트루프가 deque를 통해 받아서 이벤트를 처리하는 방식으로 동작한다. 여기서 중요한 점은 앞에서도 말했듯 해당 이벤트 핸들러를 ‘받아서’ 이벤트를 처리한다는 점이다.

이러한 이벤트 핸들러는 이벤트가 모두 실행된 후 실행되게 되는데, 이 때 실행되는 이벤트 핸들러는 원래의 이벤트 큐가 아닌 별도의 스레드에서 실행시키면서 비동기적인 특성을 유지할 수 있다.

해당 패턴은

  1. 요청을 들어온 순서대로 저장
  2. 요청 보내는 곳에서는 요청을 큐에 넣은 뒤에 결과를 기다리지 않고 리턴
  3. 요청을 처리하는 곳에서는 큐에 들어있는 요청을 나중에 처리

의 과정을 지닌다. 이러한 과정은 비단 nodejs의 libuv 뿐만 아니라 다양한 곳에서도 여러 방식으로 활용되고 있다. 특히 운영체제에서 많이 쓰이는데, 다양하게 일어나는 이벤트 요청에 대해서 스레드는 한정적이기 때문에 고정적인 스레드 수를 가지는 스레드 풀을 만들어 이를 이벤트 루프와 함께 활용하면서 스레드 풀에서의 스레드가 놀지 않도록 계속해서 이벤트를 넣어주고, 실행이 끝나면 핸들러를 실행하는 식의 이벤트 루프 패턴을 사용한다. 이를 Worker Thread라고 한다.

Event Loop와 Worker Thread

WorkerThread와 Event Loop와의 관계는 플랫폼마다 다르지만 기본적인 원리는 똑같다. - node의 libuv - emitter + queue - 모바일 main thread - event / operation queue - coroutine 이벤트가 들어왔을 때, 이를 큐에 쌓아놓고 있고 이를 처리하는 루퍼가 반복되면서 주기적으로 큐의 이벤트를 꺼내 특정한 콜백(클로저)를 호출해주는 방식이다. ㄱ

Apple Platform Foundation에서의 OperationQueue

  • 동작할 클로저를 객체 단위로 만듦(Operation class)
  • operation이라는 작업 단위로 순차적으로 처리하고, 이를 동시에 처리해줄 worker를 얼마나 둘지 설정해둘 수 있음
  • 전형적인 eventLoop를 동작시키는 추상화된 동작 방식

node.js와 libuv Looper

  • 싱글 스레드에서 루프를 두는 스타일이지만 timer나 I/O pool 등에 대한 클로저가 개별적으로 설정되어 있는 형태
  • 이를 돌아가면서 각 단계마다 블록을 걸면서 순차적으로 돌아가면서 확인
  • IO가 일어나는, 즉 다른 worker Thread가 돌아가는 구조라면, 다른 OS의 worker thread와 동작하는 경우들은 개별적인 다른 쓰레드가 있고, 그 쓰레드들이 콜백을 주는 형태

IOS에서의 Main Looper

  • IOS, Android에서는 실행하는 앱 별로 다른 쓰레드가 생김
  • 그 쓰레드별로 메인 run loop(event Loop)가 생기고, 앱이 끝나면 해당 이벤트 루프 또한 끝남
  • 터치 이벤트, IO이벤트 등을 이벤트 루프에서 확인하고 화면을 그리게 됨

Android의 Main Looper

  • Android에서도 Context 단위로 쓰레드가 유지가 되고, 그 안에 루퍼가 있음
    • Context 단위는 Activity, Service, Fragment, animations, onClick 등..
  • 루퍼가 핸들러를 호출하는 구조
  • 결국 모든 비슷한 형태의 비동기 형태의 이벤트 루퍼

타이머

  • 타임 아웃이 될 때마다 등록해놓은 콜백을 실행하는 구조
  • 타이머 애니메이션을 처리하는데 있어서 스레드가 필요한가?가 중요한 포인트
  • 타이머들이 쓰레드가 필요없이 메인 쓰레드에서 동작하는 경우가 있음
    • node의 이벤트 루퍼가 대표적인 예.
  • 다른 쓰레드에 타이머를 놓게 되면 애니메이션에 의해 타이머가 멈추거나, 타이머 때문에 애니메이션이 멈추는 경우도 있음
  • 타이머가 어느 쓰레드에서 실행되는지 확인하는 것이 중요