Dart 비동기 프로그래밍: Isolates와 event loops

Dart 비동기 프로그래밍: Isolates와 event loops

Dart는 단일 스레드 언어이지만, future, streams, 백그라운드 작업 등 현대적이고 비동기적인(Flutter의 경우 반응형) 코드를 쓰는 데 필요한 모든 것을 지원해요. 이 글은 Dart의 백그라운드 작업 지원의 기초인 isolatesevent loops를 다룹니다.

출처: Dart asynchronous programming: Isolates and event loops

본문

Dart는 단일 스레드 언어임에도 future, streams, 백그라운드 작업 등 현대적이고 비동기적인 — Flutter의 경우 반응형(reactive) — 방식으로 코드를 작성하는 데 필요한 모든 것을 지원해요. 이 글은 Dart의 백그라운드 작업 지원의 기초인 isolates와 event loops를 다룹니다.

보고 듣는 것으로 배우는 걸 더 좋아한다면, 이 글의 모든 내용이 Flutter in Focus 비디오 시리즈 Asynchronous Programming in Dart에 포함된 다음 비디오에 담겨 있어요.

새 탭에서 YouTube 재생: "Isolates and Event Loops - Flutter in Focus"

아직 여기 계신다면, isolates에 대해 이야기해 볼게요.

Isolates

Isolate는 모든 Dart 코드가 실행되는 곳이에요. 머신 위의 작은 공간 같은 건데, 저마다 전용(private) 메모리 청크와 이벤트 루프를 실행하는 단일 스레드를 갖고 있죠.

isolate는 자신만의 메모리와 이벤트 루프를 실행하는 단일 실행 스레드를 가져요.

C++ 같은 많은 다른 언어에서는 같은 메모리를 공유하고 원하는 코드를 실행하는 여러 스레드를 가질 수 있어요. 하지만 Dart에서는 각 스레드가 자신만의 메모리를 가진 isolate 안에 있고, 그 스레드는 이벤트를 처리하기만 해요(자세한 건 잠시 후에).

많은 Dart 앱은 모든 코드를 단일 isolate에서 실행하지만, 필요하면 여러 개를 가질 수도 있어요. 메인 isolate에서 실행하면 프레임을 떨어뜨릴 만큼 거대한 계산을 수행해야 한다면 Isolate.spawn()이나 Flutter의 compute() 함수를 사용할 수 있어요. 두 함수 모두 숫자 계산을 처리할 별도의 isolate를 만들고, 메인 isolate는 위젯 트리를 다시 빌드하고 렌더링하는 일에 자유롭게 쓰이도록 두죠.

두 isolate는 각각 자신의 메모리와 실행 스레드를 가져요.

새 isolate는 자신만의 이벤트 루프와 메모리를 얻어요. 원래 isolate는 이 새 isolate의 부모임에도 그 메모리에 접근할 수 없죠. 그래서 이름이 isolate인 거예요. 이 작은 공간들이 서로로부터 격리(isolated)되어 있기 때문이에요.

사실 isolate들이 협력할 수 있는 유일한 방법은 메시지를 주고받는 거예요. 한 isolate가 다른 isolate에 메시지를 보내면, 받는 isolate가 그 메시지를 자신의 이벤트 루프로 처리해요.

isolate는 다른 isolate에 메시지를 보낼 수 있어요.

이런 공유 메모리 부재는, 특히 Java나 C++ 같은 언어에서 온 사람에게는 꽤 엄격해 보일 수 있어요. 하지만 Dart 개발자에게는 몇 가지 핵심 이점이 있죠.

예를 들어 isolate 안의 메모리 할당과 가비지 컬렉션은 락(lock)이 필요 없어요. 스레드가 하나뿐이라서, 스레드가 바쁘지 않으면 메모리가 변이되고 있지 않다는 걸 알 수 있어요. Flutter 앱에는 이게 잘 맞는데, 가끔 위젯을 빠르게 만들고 해체해야 하는 경우가 있거든요.

Event loops

isolates에 대한 기초 소개가 끝났으니, 이제 비동기 코드를 진짜로 가능하게 만드는 것, 바로 event loop에 대해 파고들어 볼게요.

앱의 수명을 타임라인 위에 펼쳐 놓는다고 상상해 보세요. 앱이 시작되고, 앱이 멈추고, 그 사이에 많은 이벤트가 발생해요 — 디스크의 I/O, 사용자의 손가락 탭… 온갖 종류의 일이요.

앱은 이 이벤트들이 언제, 어떤 순서로 발생할지 예측할 수 없고, 절대 블로킹되지 않는 단일 스레드로 이 모든 것을 처리해야 해요. 그래서 앱은 이벤트 루프를 돌려요. 이벤트 큐에서 가장 오래된 이벤트를 집어 처리하고, 다시 다음 걸 가져와 처리하고, 이 과정을 이벤트 큐가 빌 때까지 반복해요.

앱이 실행되는 내내 — 화면을 탭하고, 뭔가 다운로드되고, 타이머가 울리는 동안 — 이벤트 루프는 그냥 계속 돌아가면서 이벤트를 한 번에 하나씩 처리하고 있죠.

이벤트 루프는 이벤트를 한 번에 하나씩 처리해요.

동작의 틈이 생기면 스레드는 그냥 다음 이벤트를 기다리며 쉬어요. 가비지 컬렉터를 돌리거나, 커피 한 잔을 마시거나, 뭐 그런 거요.

Dart가 비동기 프로그래밍을 위해 제공하는 고수준 API와 언어 기능 — futures, streams, async와 await — 은 모두 이 단순한 루프를 기반으로, 그리고 그 주위에 지어져 있어요.

예를 들어 네트워크 요청을 시작하는 버튼이 있다고 해볼게요.

RaisedButton(
  child: Text('Click me'),
  onPressed: () {
    final myFuture = http.get('https://example.com');
    myFuture.then((response) {
      if (response.statusCode == 200) {
        print('Success!');
      }
    });
  },
)

앱을 실행하면 Flutter가 버튼을 만들고 화면에 올려요. 그러면 앱은 기다리죠. 앱의 이벤트 루프는 그저 유휴 상태로 다음 이벤트를 기다려요. 버튼과 관련 없는 이벤트가 들어와 처리될 수도 있는 동안, 버튼은 사용자가 탭하기를 기다리며 앉아 있어요. 결국 사용자가 탭하고, 탭 이벤트가 큐에 들어와요.

그 이벤트가 처리를 위해 집어 들여져요. Flutter가 살펴보고, 렌더링 시스템이 "그 좌표는 raised button과 일치해."라고 말하므로 Flutter가 onPressed 함수를 실행해요. 그 코드가 네트워크 요청을 시작하고(future를 반환), then() 메서드로 future의 완료 핸들러를 등록해요. 그게 전부예요. 루프는 그 탭 이벤트 처리를 끝내고, 이벤트는 버려져요.

onPressedRaisedButton의 프로퍼티이고 네트워크 이벤트는 future의 콜백을 사용하지만, 두 기법 모두 근본적으로 같은 일을 해요. 둘 다 Flutter에게 "나중에 특정 유형의 이벤트가 들어올지도 몰라. 그럴 때 이 코드를 실행해 줘."라고 말하는 방식이죠.

그래서 onPressed는 탭을 기다리고, future는 네트워크 데이터를 기다리지만, Dart의 관점에서 보면 둘 다 큐에 있는 이벤트일 뿐이에요.

그것이 Dart에서 비동기 코딩이 작동하는 방식이에요. Futures, streams, async와 await — 이 API들은 모두 Dart의 이벤트 루프에게 "여기 코드가 있어. 나중에 실행해 줘."라고 말하는 방식일 뿐이죠.

위의 코드 예시를 다시 보면, 이제 특정 이벤트를 위한 블록들로 정확히 어떻게 나뉘었는지 보여요. 초기 빌드(1), 탭 이벤트(2), 네트워크 응답 이벤트(3)가 있죠.

RaisedButton( // (1)
  child: Text('Click me'),
  onPressed: () { // (2)
    final myFuture = http.get('https://example.com');
    myFuture.then((response) { // (3)
      if (response.statusCode == 200) {
        print('Success!');
      }
    });
  },
)

비동기 코드에 익숙해지고 나면 이런 패턴을 도처에서 알아보기 시작할 거예요. 이벤트 루프를 이해하는 것은 더 고수준의 API로 나아갈 때 큰 도움이 될 거예요.

정리

isolates, 이벤트 루프, 그리고 Dart 비동기 코딩의 기초를 간단히 살펴봤어요. 마이크로태스크 큐가 어떻게 동작하는지 같은 더 자세한 내용을 찾고 있다면, 오래됐지만 여전히 사랑받는 아카이브 글 The Event Loop and Dart를 참고하세요.

Dart의 비동기성에 대해 더 배우려면 이 시리즈의 다음 글인 Dart asynchronous programming: Futures를 확인해 보세요.

이 글이 바탕으로 한 비디오를 만든 Andrew Brogdon에게 큰 감사를 전합니다.

더 알아보기