개요
싱글 스레드인데 어떻게?
처음 Dart가 싱글 스레드 언어라는 말을 들었을 때는 별 생각이 없었다. 그냥 그런가보다... 했었는데 Future와 Stream을 사용하다가 문득 Dart는 싱글 스레드 언어라는 것이 떠오르며 이런 생각이 들었다. '싱글 스레드인데 비동기는 어떻게 하는거지?'
싱글 스레드는 이름 그대로 Single, 하나의 스레드라는 것으로 한 번에 하나의 명령을 수행할 수 있다. 그런데 비동기를 처리하기 위해서는 스레드가 하나만 있어서는 안된다. 명령을 처리하는 스택이 하나이기에 5초를 대기하라는 명령, 혹은 1시간을 대기하라는 명령은 다른 명령들을 막아버린다. 이렇게 지연 시키는 행동을 Block이라고 하는데, 싱글 스레드라면 구조적으로 이렇게 될 수 밖에 없다는 것이다. 시간을 재는 것 또한 하나의 명령으로 들어가기 때문이다.
그래서 Dart가 어떻게 싱글 스레드임에도 불구하고 비동기를 처리할 수 있는지 알아보기로 했다.
싱글 스레드 언어들이 비동기 작업을 지원하는 방식
JavaScript도 싱글 스레드지만 비동기를 지원한다.
비동기를 지원하는 싱글 스레드 언어가 Dart만 있는 것도 아니며 Dart가 가장 유명한 것도 아니다. 가장 유명한 싱글 스레드 언어는 JavaScirpt다. JS의 동작 방식을 알고 들어가면 Dart의 비동기 작업에 대해서도 쉽게 이해할 수 있다. 그렇다면 JS는 어떻게 비동기 작업을 지원하는 것일까?
Web Api와 이벤트 루프
아래의 영상을 보면 그 방식에 대해 간단 명료하게 설명해준다. ▼
위의 영상을 간단하게 요약하면 아래의 그림과 같다. ▼
싱글 스레드인 JS가 비동기 기능을 사용할 수 있는 궁극적인 이유는 바로 Web Api와 Event Loop라고 할 수 있다. Web Api가 멀티 스레드와 같이 기능하여 JS의 내부 스택에서 동작하지 않고 외부에서 동작하여 병렬적으로 동작이 가능해진다.
비동기 명령이 들어오면 Web Api로 보내 브라우저에서 그 명령을 대신 수행해준다. 그리고 그 비동기 명령에 속해있는 Callback이 있다면, 이를 Callback Queue로 보낸다. 그리고 Event Loop는 이를 보고 있다가 스택이 비워지게 되면 해당 Callback을 넣어준다. 그래서 스레드는 하나지만, Web Api와 Event Loop가 이를 뒷받침해주기에 가능해진다.
Dart가 비동기 작업을 지원하는 방식
Dart VM & Event Loop (Future)
JS에서 Web Api와 Event Loop가 비동기를 도와줬다면 Dart는 어떤 것이 이를 도와줄까? 바로 Dart VM과 Event Loop다. 기본적인 작동 방식과 구성은 JS와 거의 동일하지만 약간의 차이가 있다. 또한 뒤에서 이야기를 하겠지만 Dart에게는 Isolate라는 것도 있다. 일단 지금은 Future에 대해서 이야기를 해보겠다.
Future와 작동 방식
우선 Future에 대해서 이야기를 해보자. Future는 Dart에서 비동기의 결과를 보여주는 핸들이다. 그러니까 Future자체가 비동기 작업을 관리해주는, 순수 Dart에서 동작하는 멀티스레딩 기능은 아니고 비동기 작업의 결과를 보여주는 것뿐이다. Future는 일종의 Wrapper이고, Future요청이 오면 Event Loop에게 전달하며 비동기와 관련된 일들은 결국에는 Event Loop와 Event Queue, Dart VM이 하게 된다.
Dart의 명령들은 메인 스레드에서 실행이 되며 명령들은 Microtask Queue에서 대기하게 된다. JS로 따지면 Stack과도 같은 존재라고 볼 수 있다. 다음으로는 비동기 작업들을 수행하는 주체는 DartVM이 있고, Callback들을 담고 있는 EventQueue와 EventQueue를 실행단으로 올리는 것을 관리하는 Event Loop가 있다. ▼
그럼 여기에 Future가 들어왔다고 해보자. Future는 비동기 작업의 결과를 보여주기에 비동기 작업이 수행이 될 예정이지만, 우선은 메인 스레드에서 할 수 있는 것들을 실행하기 위해 Microtask Queue에 들어가게 된다. 그리고 일부가 수행된 다음 비동기 작업을 마주하면 Dart VM에게 일단 그 작업을 넘긴다. ▼
비동기 작업이 수행되면, 그 내부의 Callback을 Event Queue에 담게 된다. 예를 들어 비동기 작업이 delay와 같은 거였다면, Dart VM에서 일정 시간을 기다리고, 그 내부의 함수를 Event Queue에 넣게 된다. ▼
이렇게 Event Queue에 Callback들을 담았다고 바로 실행이 되는 건 아니다. Callback은 Event Loop에 의해 관리가 되며, Event Loop는 Microtask Queue가 빌 때 까지 Event Queue가 실행되는 것을 허용하지 않는다. 하지만 Microtask Queue가 비어있다면 Event Loop는 Event Queue에 들어있던 Callback들을 하나씩 가져와서 실행시킨다. ▼
이런 일련의 과정을 통해 Future에서 요구하는 비동기 작업들을 수행시킬 수 있게 된다. 싱글 스레드이지만 Dart VM이 비동기 작업들을 수행해주기 때문에 멀티 스레드처럼 동작할 수 있게 된다. 일반적으로 비동기 작업을 지원하는 대부분의 싱글 스레드 언어들이 이런 방식으로 멀티 스레딩 기능을 제공하고 있다.
Isolate
허나 Dart에는 병렬처리를 가능케하는 한가지 기능이 더 있다. 바로 Isolate이다. Isolate는 일반적인 스레드와 유사한 역할을 하지만, 메모리를 공유하지 않고 별도의 메모리 공간을 사용하여 데이터를 격리한다. 이를 통해 Dart가 싱글 스레드임에도 안전하게 병렬 작업을 수행할 수 있게 해준다.
Isolate의 특징
Isolate는 앞에서도 언급했듯이 자체 메모리 공간을 가진다. 다른 Isolate와 메모리를 공유하지 않기에 메모리가 변형될 일, race condition이 일어날 일이 없다. 그렇다면 메모리 공유에 대해서 의문이 생길텐데, 이 부분은 메세지 패싱을 통해 이루어진다. 또한 각 Isolate는 독립적인 이벤트 루프를 가지고 있다. 각 Isolate가 자체적으로 이벤트 큐를 처리하고 비동기 작업을 수행할 수 있다.
Future와는 뭐가 달라?
Isolate와 Future모두 비동기 작업을 가능하게 해주는 점은 동일하지만 사용처에 차이가 있다. 둘 다 비동기 작업이긴 하지만 특화된 부분이 조금씩 다르다.
Future의 경우 I/O 작업과 UI와 관련된 작업을 할 때 적합하다. Isolate도 이 작업을 수행할 수는 있지만, 메세지 패싱과 Isolate자체에서 메모리를 많이 사용하는 문제점이 있어 I/O 작업과 UI와 관련된 작업을 할 때에는 오버헤드가 발생할 수 있다. 단순히 네트워크 요청이나 파일을 읽고 쓰는데에는 Future가 Isolate보다 더 합리적이다.
반대로 Isolate는 데이터를 변환하거나 이미지 처리와 같이 CPU를 많이 사용하게 되는 복잡한 계산과 작업들에 적합하다. 일반적으로 우리가 사용할 일은 없지만, Flutter 공식 문서에 따르면 Isolate를 사용해야할 때를 제시하는데, 그게 바로 대규모 연산으로 인해 UI가 끊기는(jank)현상이 발생할 때다.
이런 식으로 크게 봤을 때는 둘이 하는 일은 같지만 세부적으로는 둘이 하는 일에는 차이가 존재한다.
Isolate를 가능하게 하는 주체와 Dart는 멀티 스레드? 싱글 스레드?
그러면 이런 의문이 들 수 있다.
'별도의 메모리 공간을 사용하여 독립적인 공간을 만들어주는 주체가 뭐야?'
Dart는 싱글 스레드 언어이고 이 작업을 수행하려면 결국에는 스레드를 사용해야한다. 앞의 Future핸들과 같이 Dart VM의 도움을 받을 수 있는 상황이면 이런 동작이 쉽게 이해가 되지만, Dart 자체만으로 가능하다고 하니 조금은 의아한 부분이 생긴다.
이 질문에 대한 해답은 Dart VM과 운영체제의 스레드이다. Dart VM이 운영체제에서 필요한 만큼의 독립적인 공간을 만들고 운영체제의 스레드 위에 올려 이 작업을 수행한다. 이 이야기를 들으면 연쇄적으로 이런 의문도 들게 된다.
'운영체제의 스레드를 사용하는거면 결국엔 멀티 스레드 언어인거 아닌가?'
궁극적으로 여러 개의 스레드를 사용하니 멀티 스레드라고 생각될 수 있지만 기본 이벤트 루프는 메인 스레드에서 실행이 된다. Isolate 외의 모든 명령은 메인 스레드에서 동작한다. 또한 Isolate는 기존의 멀티 스레드 언어들이 제공하는 메모리 공유도 지원하지 않는다. 그렇기에 이런 특징들로 인하여 Dart를 멀티 스레드가 아닌 싱글 스레드 언어라고 부른다.
마치며
간단하게 Dart가 비동기를 지원하는 방식에 대해 알아보게 되었다. 아직도 내부에서 비동기를 지원하는 방식의 대부분이 블랙박스로 감춰져있지만, 언제 Isolate가 적합하고 언제 Future를 써야하는지가 명확하게 잡혔다. 물론 일반적으로는 Isolate를 사용할 일이 없겠지만, 추후에 이미지 프로세싱이나 인공지능과 같은 조금 무거운 작업들을 클라이언트에서 돌리게 될 때 이를 기억하고 있다가 Isolate로 비동기를 구현하게 될 거 같다.
'Develop > Flutter' 카테고리의 다른 글
[Flutter][Error] Lexical or Preprocessor Issue (Xcode): 에러 해결 (0) | 2024.10.16 |
---|---|
[Flutter] Event Bus 패턴 (0) | 2024.09.28 |
[Flutter] Dart의 컴파일 과정 (0) | 2024.08.04 |
[Flutter][Widget] CustomPaint로 나만의 위젯 만들기 (2) | 2024.04.08 |
[Flutter] 패키지 사용법 (0) | 2024.04.01 |