본문 바로가기

Dart

Dart의 동시성 정리 - Isolate

동시성(Concurrency)

 

동시성(Concurrency)과 병렬성(Parallelism)

동시성(Concurrency은 Task들이 빠르게 전환하면서 실행되어 동시에 실행되는 것처럼 보이는 것입니다. 싱글 코어에서도 여러 작업은 동시적(Concurrent)으로 실행되며 Task간의 Context Switch가 발생하면

velog.io

Task들이 빠르게 전환하면서 동시에 실행되는 것처럼 보이는 것, 동시성은 싱글 코어에서만 실행되는 것을 의미하지 않고 실제로는 멀티코어에서 실행되는 상황이 더 많다. 즉 동시성은 독립적인 연산은 작은 단위의 연산으로 나누고, 논리적으로 동시에 실행하는 것처럼 보이게 하여 Idle Time을 최소화 하는 구조나 개념을 의미한다.

Isolate란?

- 앱의 모든 Dart 코드는 isolate에서 실행된다.

- isolate는 프로세스, 스레드와 비슷하지만 고유한 메모리와 이벤트 루프를 작동시키는 단일 스레드를 갖고 있다.

- isolate는 다른 isolate와 가변적인 객체를 공유하지 않으며, isolate 간 커뮤니케이션은 메시지 패싱을 통해 이루어진다.

Isolate 작동방식

대부분의 디바이스들은 멀티 코어 CPU를 가지고, 종종 많은 코어를 활용하기 위해 동시에 실행되는 공유 메모리 스레드를 사용한다.

-> 공유 상태 동시성은 에러 발생 위험이 크고, 복잡한 코드로 이어질 수 있음

 

Dart 코드는 스레드가 아닌 isolate 내부에서 실행된다. isolate는 고유한 메모리 힙을 가지고, 다른 isolate에서 자신의 상태에 접근할 수 없다. 공유하는 메모리가 없기 때문에 싱글 스레드가 많을 때 리소스에 대한 접근을 제어하는 락(뮤텍스)를 고려할 필요가 없다.

 

Isolate를 사용하면 추가 프로세서 코어를 사용하여 여러 독립적인 작업을 한번에 수행할 수 있다.

하지만 isolate를 전혀 고려하지 않아도 되는 케이스가 대부분이다.

Main Isolate

main isolate는 프로그램이 시작되는 스레드이다. 기본적으로 Dart 앱은 main isolate 위에서 실행된다.

async-await을 잘 활용하여 비동기 작업이 끝날 때까지 기다렸다가 다음 작업이 실행되도록 하면 단일 isolate 만으로도 앱 실행이 가능하다. 잘 작성된 앱은 빠른 시작 후에 빨리 이벤트 사이클에 진입하고, 필요하다면 비동기 명령을 사용하여 큐에 대기중인 이벤트에 바로 응답한다.

Isolate 생명 주기

위 그림처럼 isolate는 main()과 같은 Dart 코드가 실행될 때 생성된다. 이 Dart 코드를 통해 파일 입출력과 같은 이벤트 리스너를 등록할 수 있다. isolate에서 실행된 Dart 코드가 종료되더라도 이벤트를 처리해야하는 경우에는 isolate가 계속 유지되고, 이벤트가 종료되면 isolate도 같이 종료된다.

이벤트 처리

main isolate에는 Repaint, Tap 또는 기타 UI 이벤트가 포함될 수 있다. 이러한 이벤트가 이벤트 큐에 진입하고, 이벤트 루프는 FIFO(선입선출) 방식으로 이벤트를 처리한다.

위 이미지에서 main() 함수가 실행되면 이벤트 큐의 처리가 시작되고, Repaint 처리가 끝나면 Tap 이벤트 처리, 다시 Repaint...

처럼 순차적으로 처리된다.

동기 명령이 긴 처리 시간을 소요하면 앱은 버벅일 수 밖에 없다. 위 이미지에서 Tap 이벤트 처리 시간이 길어진 탓에 Repaint 처리가 늦어진다. 이렇게 되면 앱은 멈춰있는 것처럼 보이게 되고, 앱이 수행하는 애니메이션에서 버벅임이 보일 것이다. 랜더링 퍼포먼스는 앱 사용성과 직결되기 때문에 이러한 측면을 고려해 보는 것도 좋을 것 같다.

백그라운드 워커

큰 JSON 파일을 파싱하는 것처럼 긴 시간을 소요하는 처리때문에 UI가 반응하지 않는다면, 해당 처리를 워커 isolate로 옮김으로써 해결할 수 있다. 이 워커 isolate을 백그라운드 워커라고 부른다. 백그라운드 워커는 종료되면 메시지를 통해 결과를 반환한다.

각 isolate는 메시지를 통해 객체를 주고 받을 수 있다. 이 객체의 모든 내용은 전달 가능한 조건을 만족해야하고, 조건을 만족하지 않는다면 메시지 전송에 실패하게 된다. 예를 들어 List<Object>를 전송하려고 할 때, List에 Socket이 포함되어 있다면 실패하게 된다.

** 메시지 전송이 가능한 객체와 불가능한 객체: https://api.dart.dev/stable/3.2.0/dart-isolate/SendPort/send.html 

 

백그라운드 워커는 파일 입출력, 타이머 설정 등을 수행할 수 있다. isolate는 자신만의 고유한 메모리를 가지고 있고, main isolate와 상태를 공유하지 않는다. 따라서 워커 isolate(백그라운드 워커)를 block해도 다른 isolate에 영향을 미치지 않는다.

Code Sample

Flutter 사용 시에는 Isolate.run() 대신 compute() 사용이 권장된다. compute() 단일 함수를 워커 isolate(백그라운드 워커)로 호출하는 방법이다.

void main() async {
  final jsonData = await _fetchFile(); // 워커 isolate는 main isolate에 결과 전달, 이때 데이터 복사가 아니라 결과를 홀딩하고 있는 메모리를 main isolate에 전달
  print('Number of JSON keys: $jsonData');
}

Future<Map<String, dynamic>> _fetchFile() async {
  final fileData =
      await File("fileName")
          .readAsString();
  return compute(_parseFile, fileData); // 백그라운드 워커 생성하고 인자로 넘겨진 _parseFile 실행
}

Future<Map<String, dynamic>> _parseFile(String file) async {
  final jsonData = jsonDecode(file);
  return jsonData;
}

출력결과

 

Isolate 사이에 다수 메시지 전송

Isolate를 더 정밀하게 제어하고 싶다면 저수준API(Isolate.spawn())을 사용하면 된다. Isolate.run()은 하나의 메시지를 반환하고 Isolate를 셧다운한다. 다수의 메시지를 전송하고 싶다면 SendPort 클래스의 send() 메서드를 사용하면된다.

참고: https://github.com/dart-lang/samples/blob/main/isolates/bin/long_running_isolate.dart

 

 

'Dart' 카테고리의 다른 글

Dart 3 업데이트 문법  (1) 2023.11.13
Dart Compiler  (0) 2023.02.20