Isolate 가 뭘까?
Dart 는 많은 다른 프로그래밍 언어와 마찬가지로 스레드를 사용하여 여러개의 작업을 동시에 처리한다. 그러나, 일반적인 많은 언어들과 달리 Dart 의 스레드는 메모리를 공유하지 않는다.
Dart 에선 이러한 특별한 방식의 수행 스레드를 Isolate 라고 정의한다. 각각의 isolate 는 다른 힙 영역을 사용하기 때문에 멀티 스레딩의 비교적 복잡한 문제인 동기화에 대하여 고려할 필요가 없다. 즉, race conditions 같은 문제가 기본적으로 발생하지 않는다.
왜 Isolate 를 사용하는가?
간단한 답변으로 코드를 병렬적으로 수행하기 위함이다. 특히 멀티코어를 사용할 경우엔 물리적으로 병렬적인 코드가 수행되어 훨씬 빠른 속도를 퍼포먼스를 보일 것이다. 그러나 이 답변은 왜 멀티 스레딩을 사용하는가에 대한 답변과 크게 다르지 않다.
Isolate 는 앞서 말했듯 메모리를 공유하지 않기 떄문에 동기화 관련한 다양하고 심각한 문제들을 사전에 방지할 수 있고, 더 나아가 비교적 무거운 작업인 garbage collection 기능 또한 단순해 진다.
하지만, 분명히 isolate 간에도 데이터를 교환해야 하는 경우가 존재하는데, 이러한 기능이 일반적인 멀티 스레딩에 비하여 오래 걸리고, 복잡하다.
이는 결국 trade-off 관계이며 dart 는 메모리를 공유하지 않았을 때의 장점에 손을 들어주고, 데이터 공유를 위한 메커니즘을 따로 제공하는 방식을 선택한 것이다.
Garbage collection 이 무엇이고, isolate 가 어떻게 이를 더 단순하게 해준다는 것인가?
Garbage collection 은 많은 프로그래밍 언어에서 제공하는 자동 메모리 회수 기능이라고 볼 수 있다. (C, C++ 에선 개발자가 직접 free() 등을 활용해 회수해야 한다)
Dart 와 같은 garbage-collected 언어를 사용한다면, 새로운 객체가 생성된 후에 더 이상 사용하지 않게 되었을 때 garbage collector 가 이에 대한 메모리 회수 동작을 수행한다. 즉, garbage collector 또한 하나의 분리된 스레드라고 볼 수 있다. (isolate 는 아니다. 다른 메모리 영역에 침범해야 한다. flutter engine 의 일부 기능이자 스레드라고 볼 수 있다. 이는 C++ 로 작성되었다.)
Garbage collection 은 꽤나 복잡한 동작이며, 프로그램의 실행을 중단시킬 수 있다. 이는 퍼포먼스 이슈로 이어질 수 있기 때문에 적절하게 관리될 필요가 있다.
전통적인 멀티 스레딩 환경에서 이는 더욱 복잡하게 동작한다. 메모리를 할당 해제할 때, 이를 참조하는 모든 스레드를 고려해야 하기 때문이다. 그에 비해 Dart 에서는 독립적으로 해당하는 영역의 isolate 에 대한 고려만 필요하기 때문에, 이는 garbage collection process 를 단순화 시켜 준다.
이론적으로는 garbage collector 또한 병렬적으로 수행될 수 있지만, 이는 오히려 프로그램 수행을 많이 중단시킬 수 있기 때문에 오히려 비효율 적일 수 있다. flutter 에선 generational garbage collector 하나를 운용하며, 최근에 생성된 객체를 더 높은 우선 순위로 감지하며 garbage collector 를 수행시킨다.
이는 최근에 생성된 객체일 수록 더 빨리 할당 해제될 확률이 높기 때문이며, 이에 따라 오래된 객체에는 보다 긴 주기로 garbage collection 기능을 수행한다.
Isolate 끼리의 communication 은 어떻게 가능한가?
isolate 는 기본적으로 메모리를 공유할 수 없기 떄문에 port 를 통해 메세지를 주고 받는 방식을 사용한다.
이에 관한 내용은 코드를 보며 이해하는게 빠를 것 같다.
import 'dart:isolate';
void newIsolateEntryPoint(SendPort sendPortToMainIsolate) {
ReceivePort receivePortInNewIsolate = ReceivePort();
// Send the SendPort of this isolate to the main isolate so it can send messages to this isolate
sendPortToMainIsolate.send(receivePortInNewIsolate.sendPort);
receivePortInNewIsolate.listen((message) {
print('New isolate received: $message');
});
}
void main() async {
ReceivePort receivePortInMainIsolate = ReceivePort();
// Spawn a new isolate and pass it the SendPort of the main isolate
await Isolate.spawn(newIsolateEntryPoint, receivePortInMainIsolate.sendPort);
SendPort? sendPortToNewIsolate;
receivePortInMainIsolate.listen((message) {
if (message is SendPort) {
sendPortToNewIsolate = message;
// Send a message to the new isolate once we have its SendPort
sendPortToNewIsolate?.send('Hello from main isolate!');
} else {
print('Main isolate received: $message');
}
});
}
코드의 동작을 흐름대로 살펴보자. 우선 main 함수가 동작하는 isolate 가 존재할 것이고, 해당 함수 내에서 Isolate.spawn() 을 호출하여 새로운 isolate 를 생성할 것이다. 이 때 새로운 isolate 의 초기화 함수와 부모 isolate 에게 데이터를 전달할 수 있는 송신 포트 클래스의 인스턴스 receivePortInMainIsolate.sendPort 를 넘겨준다.
자식 isolate 의 초기화 함수 newIsolateEntryPoint() 에선 스스로 사용할 수신 포트의 인스턴스를 생성한다. 이후 전달받은 부모 isolate 송신 포트에 send() 메소드를 호출하여, 자신에게 데이터를 전달할 수 있는 송신 포트 인스턴스 receivePortInNewIsolate.sendPort 를 부모에게 넘겨준다.
receivePortInMainIsolate.listen((message) {}) 핸들러에서 수신 포트로 들어온 자식 isolate 의 sendPort 를 저장하고, send() 메소드를 통해 'Hello from main isolate!' 라는 메세지를 보낸다.
최종적인 출력은 다음과 같을 것이다.
New isolate received: Hello from main isolate!
위 과정을 통해 서로간의 데이터 전송이 이루어 졌다고 볼 수 있다.
OS 레벨에서 메세지 전송이 어떻게 동작하는지 궁금하여 알아보고 싶었으나, 특별히 문서화 되어 있지 않다. Dart runtime 에서 IPC 와 같은 OS에 기능을 활용하며 이와 관련한 동작을 처리해 준다는 정도만 이해하고 넘어가자.
Flutter 에선 isolate 를 주로 어떻게 활용할까?
Flutter 에서 사용하는 Isolate 는 크게 2가지 유형이라고 생각하면 된다. UI 가 그려지는 UI Isolate (Main Isolate) 가 있으며, 그 외에 작업을 처리하는 Worker Isolates 들이 있다.
UI Isoalte 는 모든 Dart 어플리케이션 코드를 수행하며, Flutter 화면 렌더링 엔진과 직접 상호작용하고, 유저의 입력과 같은 여러가지 이벤트를 처리하는 역할을 한다.
그 외에 개발자가 코드를 작성할 때 프로그램의 흐름을 방해할 만큼의 무거운 작업은 Worker Isolate 를 생성하는 방식으로 다른 isolate 에 위임할 수 있다. 처리가 완료되면 위에서 설명한 port 를 통한 메시지 교환 방식으로 UI Isolate 에 전달될 것이다.
이 시점에 크게 헷갈렸던 것은, 왜 네트워크 요청이나 파일을 읽는 등의 I/O 작업을 위한 isolate 는 존재하지 않냐는 것이다.
I/O 작업은 어플리케이션의 isolate 가 아닌 Flutter 의 I/O 스레드에서 처리된다.
물론, 이론적으로는 I/O 를 처리하는 특별한 isolate(worker Isolate 이다)를 생성하여 사용할 수도 있을 것이다. 하지만 이는 OS와 상호작용 하는 I/O 작업에 isolate 간의 commnuication 비용이 추가되어야 하기 때문에 비효율적이다.
Flutter 에선 위에서 설명한 대로 모든 Dart 코드는 UI Isolate 가 수행한다. 하지만 그 외 기능별 주요 작업은 다른 스레드에 의하여 수행된다. 즉, Flutter engine 은 여러 스레드를 통해 동작시키는데, UI thread 는 그 중 일부인 것이다. 이 부분을 확실히 이해하고 넘어가야 한다.
Flutter 가 사용하는 퍼포먼스와 관련한 스레드는 UI thread 포함 크게 4가지가 존재한다. iOS, Android 와 같이 네이티브 환경에 맞게 코드를 동작시키기 위해 필요한 Platform Thread 가 존재하며, 이는 각각 Objective-C, Java 등으로 작성되었다. 또한, 어플리케이션 UI 를 실제로 화면에 paintng 하기 위해 필요한 Raster Thread 가 있으며, 이는 C++ 로 작성되었다. 마지막으로 I/O Thread 또한 Flutter eninge 에 의하여 관리되며, 여러가지 I/O 작업을 대신 수행한다. 이 역시 C++ 로 작성되었다.
즉, Flutter 가 어플리케이션을 구동하기 위해선 다양한 언어로 이루어진 여러개의 스레드를 사용한다. 이전 헷갈렸던 부분은 이러한 큰 동작을 이해하지 못하고, Isolate 에 관해서만 생각하다 보니 발생했던 문제로 보인다.
이제, 다시 UI isolate 에 집중해 보자.
UI isolate 에 사용되는 Dart Runtime 시스템은 무엇일까?
먼저, runtime 이란 소프트웨어의 조각이 실행될 수 있는 환경이라고 이해할 수 있다. 프로그램 수행을 위한 필수적인 서비스들을 제공하고, 메모리와 CPU와 같은 리소스를 관리한다. 쉽게 말해, runtime 이란 개발자가 짠 코드가 동작할 수 있게 해주는 인프라인 것이다.
Dart's runtime 은 여러가지 핵심 동작을 수행한다. 먼저 Dart VM 은 Dart code 를 실행하는 엔진으로, just-in-time (JIT) 컴파일과 ahead-of-time (AOT) 컴파일의 기능을 지원한다. JIT는 빠른 개발과 디버깅을 가능하도록 해주며 개발시에 활용된다. AOT는 실제 production 을 위한 컴파일에 사용된다.
Garbage Collector 는 위에서 설명하였으니 넘어가겠다.
Isolate and Event Loop 또한 중요하다. Isolate 를 제공한다는 것은 앞서 설명하였는데, 중요한 점은 각각의 isolate 가 event loop 을 가진다는 것이다. 이를 통해 어플리케이션은 long-running task 를 수행하면서도 높은 반응성을 보일 수 있다. 각각의 이벤트 loop 는 한번에 한가지의 이벤트만을 처리한다.
event loop 은 처리되어야 하는 이벤트들을 저장하는 queue 라고 생각할 수 있다. 이 큐에는 다양한 이벤트가 포함되는데, user interaction (유저 화면 터치 등), timer event, I/O completions, 다른 isolate 로 부터 전달된 message 등이 있다.
event loop 에 관한 내용은 중요하고 양이 많기 때문에 별도의 포스팅에서 따로 공부하는 것이 좋을 것 같다.
[Dart] Event Loop
Dart 의 Event Loop 이란 무엇인가? 프로그램 수행중에 mouse click, file I/O 완료 등과 같은 다양한 이벤트가 비동기적으로 일어나는데, Dart 에선 이를 적절히 처리하기 위해 각 isolate 별로 event loop 를 유
sjoonb.tistory.com
그 외에도 Library 들이 runtime 시스템에 포함된다. 기본적인 기능을 제공하는 dart:core, I/O 관련 기능을 제공하는 dart:io, 수학 연산을 제공하는 dart:math 등이 포함된다.
이와 같은 구성요소는 모던 프로그래밍 언어에 공통적인 패턴이며, 이는 효율적인 시스템 자원 활용과, 반응형 어플리케이션을 제작을 위한 인프라를 제공한다.
참고 문헌
https://docs.flutter.dev/perf/ui-performance
Flutter performance profiling
Diagnosing UI performance issues in Flutter.
docs.flutter.dev
https://dart.cn/articles/archive/event-loop
Dart asynchronous programming: Isolates and event loops
Dart, despite being a single-threaded language, offers support for futures, streams, background work, and all the other things you need to…
medium.com
https://dart.dev/language/concurrency
Concurrency in Dart
Use isolates to enable parallel code execution on multiple processor cores.
dart.dev
'Flutter' 카테고리의 다른 글
Flutter User Input & Animation (0) | 2023.05.19 |
---|---|
Flutter Widgets (0) | 2023.05.18 |
How Flutter Renders Widgets (0) | 2023.05.16 |
[Dart] Event Loop (0) | 2023.05.15 |