Dart — Concurrency 총정리

Dart에서 헷갈릴 수 있는 Future, Isolate, Concurrency, Event Queue, Event Loop, Zone에 대한 개념의 정리

MJ Studio
MJ Studio

--

들어가기에 앞서

FutureStream의 사용법 같은 내용들은 다루지 않는다.

그건 JS와 비슷한 문법적 구조를 띌뿐더러 정리되는 글로써 가치가 크지 않기 때문이다.(사용법을 몰라야 된다는 것이 아니다, docs를 보는 것이 훨씬 이해가 빠르다).

이 글은 Dart의 Concurrency 관련 개념들을 숙지할 때 헷갈릴 수 있는 점을 요약하고 여러 좋은 글들의 내용들을 취합하고 약간이나마 더 깊은 부분을 정리하는 글이다.

기본적으로 글을 읽기에 앞서 IsolateFuture에 대한 기본적인 개념은 선행되어야 할 것이다.

Isolate의 특징

  • 코드가 동작하는 context의 단위이다. 이것은 실제 cpu processor level의 parallelism이 될 수 있다(하드웨어가 지원한다면).
  • thread의 특징인 memory의 공유가 없다. Dart의 설계에서 이것은 error-prone이기 때문에 제외되었다.
  • 메시지로 소통한다.
  • 각자의 memory heap이 존재하고 다른 Isolate의 memory에 접근할 수 없으므로 mutex나 lock에 대한 우려가 없다.
  • 싱글쓰레드로 동작하며 그렇다고 Isolate = enhanced Thread라고 보는 것은 위험하다.

Isolate Event Queue, Event Loop

Isolate는 Event queue와 Event loop를 가지며 이벤트를 처리한다. FIFO로 처리된다.

https://dart.dev/guides/language/concurrency

Isolate는 하나의 Event Loop와 두 개의 관련 Queue를 갖는다.

  • Event Queue: 외부의 이벤트인 I/O, mouse event, drawing event, timer, Isolate 간의 메시지 기타 등등을 가짐
  • Microtask Queue: event 핸들러는 때때로 task가 끝마쳐진 후, 그러나 event loop를 처리하기 전의 시점에 처리되고 싶을 수 있다. 예를 들어, observable object가 변할 때 이를 알려주거나 몇몇의 mutation이 같이 처리되어 비동기적으로 실행되고 싶을 때이다. micro task queue는 이러한 것을 event loop보다 먼저 실행시켜 예를 들어 DOM에서 inconsistent 한 상태가 보이는 것을 방지해 주기도 한다.

결과적으로, Isolate가 실행될 때(이것이 꼭 main Isolate의 main 함수가 아닐 수 있음을 인지하자), Event Loop가 동작을 시작하며 Microtask Queue — Event Queue 순으로 처리를 한다.

그리고 더 처리할 것이 없고 main Isolate라면 앱을 종료한다.

다음과 같은 Figures를 참고하자.​

https://web.archive.org/web/20170704074724/https://webdev.dartlang.org/articles/performance/event-loop
https://medium.com/@purvajg/microtasks-and-event-loops-in-dart-9f5863f031d8

Microtask Queue에 할 일이 많다면 Event Queue의 drawing 같은 동작이 실행되지 않기 때문에 앱이 멈춘다. 왜냐면 Event Loop가 Event Queue에 있는 일들을 실행할 여유가 없기 때문이다.

우리가 task가 실행될 순서를 예측할 수 있어도, 정확히 언제 실행될지를 수 없다. 다트는 single JS처럼 thread cycle에 기반을 두기 때문이다.

JS와 비슷하게 Future.delayed를 적절한 시간으로 설정해도 정말 그 시간 이후에 호출될 것이라고 확신할 수 없다는 것을 의미한다

일단 내가 확신할 수 있는 건 그 시간에는 실행이 안 된다는 것이다.

Future는 쓰레드, 멀티쓰레딩과 관련이 있는가?

Nope. Future는 추상적 개념인 concurrency와 관련이 있고 asynchronous한 API를 제공해주는 매개체일 뿐 물리적 개념인 parallelism과 아무런 관련이 없다.

FutureIsolate의 Event Queue(혹은 특정 경우 Microtask Queue)에 이벤트를 등록하는 역할을 할 뿐이다.

그리고 async/await는 특정 Event Queue에 관심 있는 이벤트가 Event Loop에서 돌아갈 시점이 왔을 때 동작시켜주는 suspend/resume동작과 관련된 syntax sugar라고 이해해도 무방하다.

Future가 싱글쓰레드에서 동작한다면 어떻게 Future.delayed 같은 것들이 불린 후에 우리의 코드가 멈추지 않고 돌아가다가 딜레이가 끝난 이후에 콜백이 실행될까?

그건 내부적으로 프레임워크가 다른 실행 컨텍스트에 동작을 위임시키고 설정된 Duration 객채에 설정된 시간 이후에 Future.delayed를 호출한 Isolate의 Event Queue에 이벤트를 삽입시켜주기 때문이다.

결과적으로 우리가 적는 모든 코드는 특별히 Isolate를 spawn 해주지 않는다면 Main Isolate에서 동작한다.

Future와 Isolate는 전혀 관계없는 것이며 대개 비동기적인 꼴로 나타나는 Isolate 간의 inter-communication을 다루기 위해 Future가 같이 사용되는 것뿐이다.

How to schedule a task

  • top-levle scheduleMicrotask() 함수를 이용하면 Microtask Queue에 콜백을 넣을 수 있다.
  • 이미 completed 된 Futurethen은 Microtask Queue에 삽입된다.​

웬만하면 Event Queue를 사용하라. Microtask queue의 callback들로 Event Loop가 blocking 되면 Event Queue의 동작들이 실행되지 않기 때문이다. 그리고 앱에선 흔히 Drawing이 지연되기 때문에 버벅거리는 앱이나 ANR 상황이 올 수 있다.

Task가 절대적으로 Event Queue의 것들보다 먼저 처리되어야 한다면 그냥 즉시 호출하거나 Microtask queue 에 스케줄링 할 수 있다. 예를 들어, 버튼 클릭 이벤트보다 항상 먼저 처리되길 바란다면 그렇다.

The facts about Future

  • Future는 Event Queue에서 실행된다.
  • thenFuture가 complete된 이후 즉시 실행된다.
  • 단, Future가 이미 completed인 Future일 경우, microtask queue에 then 콜백이 들어간다. 이러한 예시엔 Future.value, Future.sync(함수가 Future를 반환하지 않는 경우)가 있다.

​Quiz

지금까지 잘 이해했다면 다음과 같은 퀴즈가 어렵지 않아야 한다.

사실 어렵다. futureSyncReturnFutureValue 를 조심하자.

1
9
4
6
7
8
2
3
5

두 번째 퀴즈다.

이건 다음과 같은 글에서 가져왔다.

main #1 of 2
main #2 of 2
microtask #1 of 3
microtask #2 of 3
microtask #3 of 3
future #2 of 4
future #2a
future #2b
future #2c
microtask #0 (from future #2b)
future #3 of 4
future #4 of 4
future #3a (a new future)
future #3b
future #1 (delayed)

결과가 억까당하는 기분이 든다면 위 글을 참고하자.

microtask #0의 결과가 의아할 수 있는데, then에서 실행되는 저 친구는 then의 체인 이후에 Microtask Queue에 들어가는 것으로 보인다.

위 글에서 말하는 Bug 9001/9002(scheduleMicrotask가 처음에 Event Queue로 들어가는 버그)는 현재 고쳐진 것으로 보이며 따라서 글의 결과와 실제 돌려봤을 때 결과가 다르다.

Zone

엄밀히 말하자면 Future.delayed는 내부적으로 Timer를 쓰고 Timer는 내부적으로 Zone을 쓴다. Zone이란 클래스의 정의엔 다음과 같이 쓰여있다. 어떻게 알았냐면 직접 까봤다.

This is all handled internally by the platform code and most users don’t need to worry about it. However, developers of new asynchronous operations, provided by the underlying system, must follow the protocol to be zone compatible.

결국 내부적으로 platform code로 다루어진다는 뜻인데, 좀 더 뜯어보았다.

File.read 같은 경우는 내부적으로 타고들어가다보면 external function으로 정의되어있으므로 추적에 실패했다.

Zone은 하나의 Isolate 안의 virtual area로 볼 수 있다.

그외 Zone의 기능/특징은 다음과 같다.

  • Isolate의 변수에 접근하고 Isolate에서 코드를 실행시킨다.
  • 미리 정의된 default behavior(print, timer, microtask, error handler)를 사용할 수 있다.
  • 실행점에 대한 훅을 제공한다.

Isolate의 entry point는(Main Isolate에서의 main()인 그것) 기본적으로 Zone.root에서 시작된다.

여기서 말하는 default behavior란 그 환경에서 던져진 에러를 처리하는 다른 방식을 정의할 수 있다는 것이다.

https://medium.com/@valiodas/dart-exploring-zones-prints-41c8a44e139a

결론적으로 ZoneIsolate에 종속적이다.

Custom Zone을 새롭게 만들면, 해당 Zone에 대한 global한 uncaught catch block을 정의할 수 있다.

Hi I am in a new zone
1

이후에 ZoneSpecification 클래스를 적절히 정의한 후 Zone을 만들 때 전달하면 커스텀한 print나 에러 핸들러를 만들 수 있다.

내부적으로 Zone을 만들때는 Zone.fork()가 사용되고 작동되는 방식은플랫폼 종속적으로 보인다.

자세한건 다음과 같은 글을 참고하라.

https://medium.com/@valiodas/dart-exploring-zones-prints-41c8a44e139a

--

--