[Dart] Isolate에 대해 알아보자

2024. 7. 29. 10:49Programming/Flutter

반응형

#1. Dart와 동시성

Dart는 기본적으로 싱글 스레드 환경에서 동작합니다. Javascript에 익숙하다면, 여기까지만 들어도 '엥? 그럼 혹시 Dart도 비동기 처리는 Javascript처럼...?'이라는 생각이 들 텐데요. 그렇습니다. Javascript와 마찬가지로 Dart 역시 이벤트 루프를 가지고 있으며, 비동기 처리를 진행하는 방식은 Javascript와 비슷합니다. 이벤트 루프에 대해 자세히 설명되어있는 글들은 여러가지가 있겠지만, Dart의 이벤트 루프에서는 Dart 문서에 기재되어있는 Dart의 동시성 항목을 참고해주세요. 대충 설명하면 Dart로 작성된 프로그램이 실행되면 이벤트들이 저장되는 이벤트 큐가 있고, 우리가 Dart로 작성하지 않아도 이벤트 큐에서 대기하고 있는 이벤트들을 주기적으로 처리한다고 생각하면 됩니다. 대충 Flutter를 사용해서 작성한 페이지를 화면에 띄운 뒤, 더 이상 할 게 없어진 Flutter가 혹시 남은 일이 있는지 이벤트 큐를 뒤져보는 걸 상상해보면 이해하기 쉽습니다.

http.get()로 비동기 요청을 하는 코드를 통해, then()으로 생성한 클로저가 이벤트 루프를 통해 실행되는 과정을 설명한 그림 (Dart의 동시성 참조)

#2. 싱글 스레드가 가져오는 한계

Flutter로 앱을 작성하게되면, 우리가 작성한 코드는 동시에 여러가지에 작업을 하게 됩니다. 서버에 HTTP로 데이터를 요청할수도 있고, 어딘가에 저장되어있는 데이터를 불러올수도 있죠. 동시에 화면도 그려야 하는데, 데이터를 불러오는데 시간이 오래 걸린다면 사용자에게 데이터를 불러오고 있다는 것을 알려줘야합니다. 그래야 사용자가 '이 앱 완전히 먹통이잖아?'라면서 앱을 삭제하지 않고, '뭔가 불러오고 있군...'하면서 잠시 기다릴테니까요. 거기에 더해서 데이터 처리가 끝나면 사용자의 이목을 끌기 위해 애니메이션 처리를 할 수도 있습니다! 좋아요, 그렇다면 수많은 데이터를 한번에 불러오거나, 서버에 수십개의 HTTP 요청을 하면 어떻게 될까요?

결국에는 하나의 스레드가 모든 것을 처리하다보면 할당된 자원의 한계가 있습니다. 큰 데이터를 불러오면서 동시에 UI를 화면에 그리고, 수 많은 HTTP 요청을 동시에 처리하다보면, 어느 순간부터는 처리속도가 느려지게 되죠. 데이터를 불러오는 데 걸리는 시간이 길어지는 건 어느정도 타협을 볼 수 있죠. 데이터를 불러오는 시간에 영향을 미치는 것은 기기의 성능부터 해서 네트워크 상황까지, 너무나도 많은 변인이 있으니까요. 하지만 화면이 버벅이는 건 어떨까요? 긴 리스트를 스크롤하는 도중 버벅거리고, 버튼을 눌렀는데 3초 뒤에야 눌린 동작이 실행되고, 다른 화면으로 이동하는데 멈칫!하는 앱을 상상해봅시다. 결국은 데이터를 불러오는데 느리고, 화면도 버벅이면서, 버튼을 눌러도 반응이 없는 앱이 되어버린다면, 아마도 사용자는 대체제를 찾는 순간 떠나버리게 될 겁니다.

오래 걸리는 비동기 작업으로 인해 예상된 시간보다 다음에 repaint되야 할 프레임 갱신이 늦어지는 것을 설명한 도식표 (Dart의 동시성 참조)

#3. Isolate란?

Dart에서는 동시성을 지원하기 위해서 Isolate를 지원합니다. 스레드나 프로세스와 비슷하지만, Isolate라는 이름에서 알 수 있듯 메모리와 이벤트 큐를 별도로 할당받죠. 개인적으로는 이런 점 때문에 스레드보다는 프로세스에 가깝다고 생각합니다. 애시당초 spawn()으로 생성하는 것도 그렇고 말이죠. 아무튼 간단하게 설명하면 하위 프로세스라고 생각하면 쉽습니다. 여담으로 Dart로 작성한 프로그램은 여타 프로그램과 비슷하게 main() 함수부터 시작되는데요. 이 main() 함수를 실행하는 Isolate를 Main Isolate라고 한답니다.

스레드와는 달리 메모리를 공유하지 않기 때문에, 공유 자원으로 인해 비롯되는 여러가지 문제에 대해서 자유롭습니다. 물론 그렇다보니 특정한 데이터에 대한 처리를 요청하거나, 처리된 데이터를 받기 전달받기 위해서는 통신을 거쳐야하므로, 생각보다 작성해야하는 코드량이 늘어나는 경우가 있습니다. 따라서 진짜 🐶쉽게 Isolate 적용하기 - Ximya 님처럼 mixin을 사용하거나, Generic을 사용하는 등 재사용성을 고려하여 작성하는 것이 좋습니다.

Isolate의 라이프 사이클 (Dart의 동시성 참조)

#4. Isolate의 갯수

하나의 Isolate는 이벤트 루프를 실행하기 위한 하나의 스레드를 가지고 있으며, 별도의 자원을 할당받습니다. Multiple Isolates vs one Isolate - StackOverflow에 작성되어있는 답변에서 Isolate의 특징에 대해 살펴볼 수 있는데, 영향을 미칠 수 있는 요소는 여러가지이지며, 많이 생성하게되면 오히려 전체적인 성능에 악영향을 준다는 점을 시사하고 있습니다. 언급한 StackOverflow에서는 적절한 Isolate의 갯수는 CPU의 갯수±2 정도지만, 적절한 갯수는 테스트해보면서 조율할 필요가 있다고 나와있는데요. 최종적으로 타겟팅하는 기기와 구현하고자하는 내용에 따라서 달라질 수 있는 내용이므로, 가능하다면 여러 환경에서의 동작을 고려해서 최적의 갯수를 찾아내는게 이상적입니다.

하지만 최근에는 기기의 종류가 다양해지고 유럽 등지에서는 상대적으로 저렴한 보급형 기기도 늘어나고 있으므로, 개인적으로는 동시에 부하가 큰 작업을 처리하는 Isolate의 갯수는 1~2개 정도를 유지하는게 좋다고 생각합니다. Isolate를 사용하는 가장 큰 이유는 부하가 걸리는 무거운 작업을 보다 빠르게 처리하는 게 아니라, 부하가 걸리는 무거운 작업을 처리하는 동안 UI의 버벅거림을 없애기 위한 것이기 때문입니다.

Main Isolate와 Spawned Isolate의 Lifecycle (Dart와 동시성 참조)

#5. Isolate의 사용

Isolate를 사용하는 방법에는 Isolate.run()을 사용해서 실행하는 간단한 사용법과, Compute 함수를 사용해서 구현하는 다소 복잡한 사용법 두 가지가 있습니다. 이 글에서는 Dart 문서에 기재되어있는 Isolates 페이지에 개재되어있는 예제를 기반으로, 좀 더 단순하게 작성한 코드를 통해 Isolate의 구현에 대해 살펴보겠습니다. 참고로 웹에서는 Isolate를 지원하지 않으므로, Dartpad를 사용해서 예제를 구동하는 것은 불가능하므로 참고해주세요.

#5-1. 간단한 Isolate의 구현 방법

먼저 다음과 같이 Isolate.run()을 사용하면 간단하게 Worker Isolate에서 함수를 실행하는 코드를 작성할 수 있습니다. 이번에는 문자열 name과 정수 age로 구성되어있는 Json 문자열을 기반으로 생성하는 데이터 클래스 Data를 작성했습니다. 여기서 Data.fromJson() 함수를 통해 주어진 Json 문자열로부터 Data 객체를 생성하는데 15초가 걸린다는 것을 가정하고 예제 코드를 작성했습니다.

import 'dart:convert';
import 'dart:isolate';

class Data {
  final String name;
  final int age;

  Data({required this.name, required this.age});

  factory Data.fromJson(Map<String, dynamic> json) => Data(
        name: json['name'] ?? '',
        age: json['age'] ?? 0,
      );

  Map<String, dynamic> toJson() => {
        'name': name,
        'age': age,
      };
}

void main() async {
  print('main isolate start!');
  // Data 클래스로 파싱할 Json 문자열 jsonString
  const String jsonString = '{"name": "BlackBear", "age": 5}';

  Isolate.run<Data>(
    () async {
      print('worker isolate start!');
      Data result = await Future.delayed( // 작업은 15초 뒤에 완료된다.
          Duration(seconds: 15), () => Data.fromJson(jsonDecode(jsonString)));

      print('worker isolate done!');
      return result;
    },
  ).then((Data data) {
      // Worker isolate와 Main isolate가 별개로 동작하는 것을 보기 위해
    // async-await을 사용해서 대기하지 않고, then을 사용해서 콜백을 등록한다.
    print(
      'Name: ${data.name}, Age: ${data.age}',
    );
  });
  print('main isolate done!');
}

실행 결과는 모두가 예상할 수 있듯, main isolate start! ➡️ worker isolate start! ➡️ main isolate done! ➡️ worker isolate done! ➡️ Name: BlackBear, Age: 5 순서로 문자열을 출력하게 됩니다. Future.delayed()를 통해서 15초를 대기하는 동안 호출 스택에 worker isolate가 표시되는 것을 주목해주세요. worker isolate done! 문자열을 출력하고 난 뒤에는 호출 스택에서 worker isolate가 사라지는 것을 볼 수 있는데, Isolate.run()을 사용해서 Isolate를 생성하는 경우에는 Isolate에 넘겨준 함수를 실행한 뒤에 자동적으로 종료된다는 것을 알 수 있습니다.

Isolate.run()을 사용한 예제 코드를 VSCode로 실행한 결과, 자원이 자동으로 할당 해제되는 것을 알 수 있다.

좋아요, 그렇다면 이 간단한 Isolate.run() 함수가 어떻게 구현되어있는지 따라가보도록 합시다. isolate.dart 파일에 구현되어있는 static Future run(FutureOr computation(), {String? debugName})을 열어보면 다음과 같이 구현되어있는 것을 알 수 있습니다. (Flutter 3.22.3 기준)

@Since("2.19")
  static Future<R> run<R>(FutureOr<R> computation(), {String? debugName}) {
    var result = Completer<R>();
    var resultPort = RawReceivePort();
    // worker isolate가 main isolate로 보낸
    // response를 처리하기 위한 핸들러 등록
    resultPort.handler = (response) {
      resultPort.close();
      if (response == null) {
        // onExit handler message, isolate terminated without sending result.
        result.completeError(
            RemoteError("Computation ended without result", ""),
            StackTrace.empty);
        return;
      }
      var list = response as List<Object?>;
      if (list.length == 2) {
        var remoteError = list[0];
        var remoteStack = list[1];
        if (remoteStack is StackTrace) {
          // Typed error.
          result.completeError(remoteError!, remoteStack);
        } else {
          // onError handler message, uncaught async error.
          // Both values are strings, so calling `toString` is efficient.
          var error =
              RemoteError(remoteError.toString(), remoteStack.toString());
          result.completeError(error, error.stackTrace);
        }
      } else {
        assert(list.length == 1);
        result.complete(list[0] as R);
      }
    };
    try {
      Isolate.spawn(_RemoteRunner._remoteExecute,
              _RemoteRunner<R>(computation, resultPort.sendPort),
              onError: resultPort.sendPort,
              onExit: resultPort.sendPort,
              errorsAreFatal: true,
              debugName: debugName)
          .then<void>((_) {}, onError: (error, stack) {
        // Sending the computation failed asynchronously.
        // Do not expect a response, report the error asynchronously.
        resultPort.close();
        result.completeError(error, stack);
      });
    } on Object {
      // Sending the computation failed synchronously.
      // This is not expected to happen, but if it does,
      // the synchronous error is respected and rethrown synchronously.
      resultPort.close();
      rethrow;
    }
    return result.future;
  }

앞서 각 isolate는 메모리를 공유하지 않으므로, 통신을 이용해서 처리할 데이터를 주고받는다는 얘기를 했는데요. Isolate.run()을 실행하게되면 응답을 서로 주고받기 위해서 RawReceivePort resultPort를 생성하는 것을 볼 수 있습니다. 이후 RawReceivePort.handler를 통해 worker isolate가 main isolate로 보낸 응답을 처리하기 위한 핸들러를 등록하는데요. worker isolate가 전송한 response값을 List<Object?>으로 형변환한 뒤, 리스트의 길이가 2개 이상이면 result.completeError()를 호출해서 에러처리하는 것을 볼 수 있습니다. 만약 리스트의 길이가 1개이면 result.complete()를 호출해서 완료 처리를 진행합니다.

 

RawReceivePort.hanlder에 핸들러를 등록하는 코드로부터 아래로 내려오면 try-catch문을 발견할 수 있는데, 생각보다 코드는 단순합니다. Future.then()onError를 사용해서 에러가 발생할 경우에는 RawReceivePort resultPortclose()를 호출해서 Isolate를 종료하고, result.completeError()를 호출해서 에러 처리를 합니다. 최종적으로는 Completer.future을 반환해서, Isolate.run()Future를 통해 비동기 처리를 위한 콜백을 등록할 수 있도록 합니다. 이렇게 Isolate.run()이 어떻게 구현됐는지를 살펴봤는데요. 여기서 알 수 있듯 Completer를 사용하면 Isolate가 조금 더 복잡한 일을 할 수 있도록 구현할 수 있습니다.

#5-2. Completer를 사용한 Isolate 구현 방법

위에서 함께 살펴본 Isolate.run()의 구현사항을 살펴보면 Completer를 사용하고 있습니다.Completer를 사용하면 Isolate를 사용한 비동기 처리가 가능하며, ReceivePort를 통해 Worker Isolate에 메시지를 전달함으로써 여러가지 처리를 할 수 있는데요. 앞에서 Isolate의 수를 제한해야 한다는 언급을 했었는데, Worker Isolate로 전달하는 메시지를 통해서 하나의 Worker Isolate에서 실행할 내용을 다양화 할 수도 있습니다. Completer를 사용해서 하나의 Isolate를 생성하고, 여러개의 메시지를 전송하는 코드는 공식 문서의 포트를 사용해서 Isolate 사이에 여러개의 메시지를 주고받기에서 확인할 수 있으니 참조해주세요.

 

먼저 포트를 사용해서 Isolate 사이에 여러개의 메시지를 주고받기에 설명되어있는 전체 코드는 아래와 같습니다. 개인적으로는 Dart에서 제공하는 타입추론을 신뢰하지 않는 편이기에(...) 예제 코드에서 일부 타입 추론을 뺐으나, 전체적인 틀에서는 크게 다르지 않습니다. 간단하게 Worker Isolate를 spawn하고, Json 포맷으로 작성된 문자열을 Worker Isolate로 전송한 뒤, Json 객체로 파싱한 결과를 응답으로 받는 예제 코드입니다. 주된 내용은 main()함수를 보면 알 수 있으므로, 하나하나 따라가면서 살펴보도록 합시다.

import 'dart:async';
import 'dart:convert';
import 'dart:isolate';

void main() async {
  final worker = await Worker.spawn();
  print(await worker.parseJson('{"key":"value"}'));
  print(await worker.parseJson('"banana"'));
  print(await worker.parseJson('[true, false, null, 1, "string"]'));
  print(
    await Future.wait(
      [
        worker.parseJson('"yes"'),
        worker.parseJson('"no"'),
      ],
    ),
  );
  worker.close();
}

class Worker {
  final SendPort _commands;
  final ReceivePort _responses;
  final Map<int, Completer<Object?>> _activeRequests = {};
  int _idCounter = 0;
  bool _closed = false;

  Future<Object?> parseJson(String message) async {
    if (_closed) throw StateError('Closed');
    final Completer<Object?> completer = Completer<Object?>.sync();
    final int id = _idCounter++;
    _activeRequests[id] = completer;
    _commands.send((id, message));
    return await completer.future;
  }

  static Future<Worker> spawn() async {
    // Create a receive port and add its initial message handler
    final RawReceivePort initPort = RawReceivePort();
    final Completer<(ReceivePort, SendPort)> connection =
        Completer<(ReceivePort, SendPort)>.sync();
    initPort.handler = (initialMessage) {
      SendPort commandPort = initialMessage as SendPort;
      connection.complete((
        ReceivePort.fromRawReceivePort(initPort),
        commandPort,
      ));
    };

    // Spawn the isolate.
    try {
      await Isolate.spawn<SendPort>(_startRemoteIsolate, (initPort.sendPort));
    } on Object {
      initPort.close();
      rethrow;
    }

    final (ReceivePort receivePort, SendPort sendPort) =
        await connection.future;

    return Worker._(receivePort, sendPort);
  }

  Worker._(this._responses, this._commands) {
    _responses.listen(_handleResponsesFromIsolate);
  }

  void _handleResponsesFromIsolate(dynamic message) {
    final (int id, Object? response) = message as (int, Object?);
    Completer<Object?> completer = _activeRequests.remove(id)!;

    if (response is RemoteError) {
      completer.completeError(response);
    } else {
      completer.complete(response);
    }

    if (_closed && _activeRequests.isEmpty) _responses.close();
  }

  static void _handleCommandsToIsolate(
    ReceivePort receivePort,
    SendPort sendPort,
  ) {
    receivePort.listen((message) {
      if (message == 'shutdown') {
        receivePort.close();
        return;
      }
      final (int id, String jsonText) = message as (int, String);
      try {
        final jsonData = jsonDecode(jsonText);
        sendPort.send((id, jsonData));
      } catch (e) {
        sendPort.send((id, RemoteError(e.toString(), '')));
      }
    });
  }

  static void _startRemoteIsolate(SendPort sendPort) {
    final ReceivePort receivePort = ReceivePort();
    sendPort.send(receivePort.sendPort);
    _handleCommandsToIsolate(receivePort, sendPort);
  }

  void close() {
    if (!_closed) {
      _closed = true;
      _commands.send('shutdown');
      if (_activeRequests.isEmpty) _responses.close();
      print('--- port closed --- ');
    }
  }
}

#5-2-1. spawn()와 _startRemoteIsolate()

main()를 살펴보면 Worker Isolate를 생성하기 위해 Worker.spawn()을 호출하고 있습니다. 예제 코드의 spawn()를 살펴보면, 아래와 같이 구현되어있습니다.

static Future<Worker> spawn() async {
  // Create a receive port and add its initial message handler
  final RawReceivePort initPort = RawReceivePort();
  final Completer<(ReceivePort, SendPort)> connection =
      Completer<(ReceivePort, SendPort)>.sync();
  initPort.handler = (initialMessage) {
    SendPort commandPort = initialMessage as SendPort;
    connection.complete((
      ReceivePort.fromRawReceivePort(initPort),
      commandPort,
    ));
  };

  // Spawn the isolate.
  try {
    await Isolate.spawn<SendPort>(_startRemoteIsolate, (initPort.sendPort));
  } on Object {
    initPort.close();
    rethrow;
  }

  final (ReceivePort receivePort, SendPort sendPort) =
      await connection.future;

  return Worker._(receivePort, sendPort);
}

Worker._(this._responses, this._commands) {
  _responses.listen(_handleResponsesFromIsolate);
}
...


static void _startRemoteIsolate(SendPort sendPort) {
  final ReceivePort receivePort = ReceivePort();
  sendPort.send(receivePort.sendPort);
  _handleCommandsToIsolate(receivePort, sendPort);
}

spawn()함수에서는 통신에 사용할 포트를 초기화하기 위한 RawReceivePort initPort을 선언한 뒤, Compeleter<(ReceivePort, SendPort)> connection을 선언합니다. 이후 initPort.handler에 포트를 등록하기 위한 핸들러 함수를 등록하고, Isolate.spawn<SendPort>을 호출하는데, 인자값으로 전달된 _startRemoteIsolateinitPort.sendPort를 통해서 메시지를 주고받기위한 포트를 설정합니다. 이후 메시지를 수신할 ReceivePort receivePort와 메시지를 전송할 SendPort sendPortconnection.future로 할당한 다음, 생성자 Worker._()를 통해 Worker 객체를 생성합니다.

 

Worker._()는 생성자로 _response.listen()함수를 호출해서 Worker Isolate로부터 메시지를 수신했을 때,_handleResponseFromIsolate()를 실행하도록 _response.listen()에 콜백으로 등록하고 있습니다.

Isolate.spawn()에 인자값으로 전달한 _startRemoteIsolate() 함수를 보면 역시 ReceivePort()를 선언한 뒤, 인자로 전달받은 SendPort sendPort로 자신이 생성한 ReceivePort의 receivePort.sendPort를 전송하는 것을 볼 수 있다. 이후 _handleCommandsToIsolate() 함수를 호출하는데, 이 함수에서 receivePort로 전달받은 메시지를 처리한 뒤 결과값을 sendPort로 전송한다는 것을 추측해볼 수 있습니다.

#5-2-2. _handleCommandsToIsolate()

5-2-1에 설명된 내용을 보면 Main Isolate가 Worker Isolate로 메시지를 전송했을 때, Worker Isolate에서 실행되는 내용은 _handleCommandsToIsolate()에 작성되어있다는 것을 알 수 있습니다. _handleCommandsToIsolate()함수의 구현은 아래와 같습니다.

static void _handleCommandsToIsolate(
  ReceivePort receivePort,
  SendPort sendPort,
) {
  receivePort.listen((message) {
    if (message == 'shutdown') {
      receivePort.close();
      return;
    }
    final (int id, String jsonText) = message as (int, String);
    try {
      final jsonData = jsonDecode(jsonText);
      sendPort.send((id, jsonData));
    } catch (e) {
      sendPort.send((id, RemoteError(e.toString(), '')));
    }
  });
}

message'shutdown'일 경우에는 receivePort.close()를 호출해서 포트를 닫습니다. 만약 message'shutdown'이 아닐 경우에는 (int, String)라고 간주하여 형변환을 한 뒤, jsonDecode()함수로 문자열을 파싱한 결과를 sendPort로 전송합니다. 여기서는 messageString(int, String) 두 가지의 타입이 될 수 있으므로, messagedynamic 타입이 됩니다.

 

여기서 Worker Isolate에서는 closeparseJson 두 개의 동작만을 수행하지만, message를 다양하게 구현하면 하나의 Worker Isolate에서 여러개의 동작을 처리하게끔 만드는 것도 가능합니다. 만약 Worker Isolate에서 여러개의 작업을 처리해야한다면, 이 내용에 주목할 필요가 있습니다.

#5-2-3. parseJson(),_handleResponsesFromIsolate(), close()

남은 함수들은 Worker Isolate에서 Json 형식 문자열을 처리하기 위해 호출하는 parseJson()와, Worker Isolate에서 처리한 내용을 전달받아 Main Isolate에서 처리하기 위한 _handleResponsesFromIsolate(), 그리고 포트를 닫고 Worker Isolate를 종료하기 위한 close()입니다. 하나하나 살펴보도록 하죠.

Future<Object?> parseJson(String message) async {
  if (_closed) throw StateError('Closed');
  final Completer<Object?> completer = Completer<Object?>.sync();
  final int id = _idCounter++;
  _activeRequests[id] = completer;
  _commands.send((id, message));
  return await completer.future;
}

void _handleResponsesFromIsolate(dynamic message) {
  final (int id, Object? response) = message as (int, Object?);
  Completer<Object?> completer = _activeRequests.remove(id)!;

  if (response is RemoteError) {
    completer.completeError(response);
  } else {
    completer.complete(response);
  }

  if (_closed && _activeRequests.isEmpty) _responses.close();
}

void close() {
  if (!_closed) {
    _closed = true;
    _commands.send('shutdown');
    if (_activeRequests.isEmpty) _responses.close();
    print('--- port closed --- ');
  }
}

먼저 parseJson() 함수를 살펴봅시다. parseJson()은 새로운 id를 생성하여 _activeRequests에 새로운 Completer<Object?>를 할당함으로써, Worker Isolates에서 수행할 작업들을 관리합ㄴ디ㅏ. 이후 SendPort _commands에 새로운 id와 Json 형식의 문자열 message을 전송하고, completer.future를 반환합니다. completer.future는 이후 completer.complete()혹은 completer.completeError() 함수가 실행되면 처리되는데, completer.complete()completer.completeError() 모두 _handleResponsesFromIsolate() 함수에 선언되어있다는 것에 주목해주세요.

 

코드의 흐름을 처음부터 따라가보면 다음과 같습니다. main()에서 선언되어있는 것을 보면 알 수 있듯, Worker.spawn()함수를 통해 생성된 Worker객체가 생성됩니다. 이후 parseJson()을 통해 처리할 문자열이 Worker Isolate로 전달되면, Worker Isolate에서 동작하는 _handleCommandsToIsolate()함수가 문자열을 처리한 뒤 결과값을 Main Isolate로 전달합니다. 마지막으로 Main Isolate에 선언된 _handleResponsesFromIsolate()가 메시지를 수신하면 _activeRequestsid값으로 등록해놓은 Completer를 찾아서, 결과값에 따라 completer.completeError() 혹은 completer.complete()를 실행하게 되는데, 이는 parseJson()에서 반환한 completer.future의 결과값이 되어 비동기 처리됩니다.

마지막으로 Worker객체의 close()함수를 실행하게되면 shutdown 메시지를 전송하게되는데, 이 메시지를 수신한 Worker Isolate는 포트를 닫고 종료하게 됩니다. _handleResponsesFromIsolate() 함수에서도 _closed 플래그를 보고 _responses.close()를 요청하는 내용을 확인할 수 있는데, 이는 Worker Isolate에서 처리된 결과값을 전달받았으나 이미 Worker.close()가 호출됐을 때 자원을 정리하기 위함입니다.

 

이로써 포트를 사용해서 Isolate 사이에 여러개의 메시지를 주고받기에 나와있는 예제를 모두 살펴봤습니다. 다음 글에서는 제한된 Isolate로 보다 여러개의 작업을 처리할 수 있도록, 예제 코드를 수정해보도록 합시다.

반응형