플러터 - Isolate

슬로우플랫폼·2025년 1월 13일
0

안녕하세요. 슬로우의 Shawn입니다.

슬로우톡 앱에서 주소록 친구 추가를 하기 위해, 주소록을 불러오고 연락처를 파싱하는 작업을 진행하는데, 시간이 너무 오래 걸리는 문제가 있었습니다.

이에 Background로 주소록 데이터를 불러와서 작업을 하면 어떨까 하는 생각에 플러터에서 Background 작업을 지원하는 것을 찾게 되었습니다.

플러터 공식 홈페이지와 여러 블로그들을 찾아 보니, Isolate라는 동시성을 지원하는 기능이 있었습니다. 이 Isolate라는 것에 대해 알아보려 합니다.

플러터는 기본적으로 단일 스레드(Single Thread)에서 동작을 합니다. 멀티 스레드와 같은 것을 지원하기 위한 이 Isolate에 대해 플러터 공식 홈페이지에는 아래와 같이 설명하고 있습니다.

Isolate

Isolate
Dart 코드의 모든 작업은 고립된 메모리를 가진 아이솔레이트(isolate)에서 실행됩니다. 아이솔레이트는 스레드와 유사하지만, 상태를 공유하지 않으며 메시지를 통해서만 통신할 수 있다는 점에서 다릅니다. 기본적으로 Flutter 앱은 모든 작업을 단일 아이솔레이트(메인 아이솔레이트)에서 실행합니다. 대부분의 경우, 이 모델은 프로그래밍을 더 간단하게 만들어주며, 애플리케이션의 UI가 반응하지 않게 되는 상황을 방지할 만큼 충분히 빠릅니다. (링크)
Dart에서는 스레드 대신 모든 코드가 Isolate 안에서 실행됩니다. Isolate를 사용하면 Dart 코드가 여러 독립적인 작업을 동시에 수행할 수 있으며, 가능한 경우 추가 프로세서 코어를 활용할 수 있습니다. Isolate는 스레드나 프로세스와 비슷하지만, 각 Isolate는 고유한 메모리를 가지며, 이벤트 루프를 실행하는 단일 스레드로 작동합니다. (링크)

위 설명에서 보듯이 스레드와 Isolate는 같은 것이 아니며, 차이점은 아래와 같이 정리할 수 있습니다.

Thread vs Isolate

스레드 (Thread):

  • 공유 메모리: 스레드는 동일한 메모리 공간을 공유합니다. 여러 스레드가 같은 변수나 데이터를 동시에 읽고 쓸 수 있습니다.
  • 동기화 문제: 공유 메모리로 인해 동시성 제어를 위한 락(lock)과 같은 동기화 메커니즘이 필요합니다. 이를 잘못 다룰 경우 교착 상태(deadlock)이나 경쟁 상태(race condition)와 같은 문제가 발생할 수 있습니다.
  • 오버헤드: 공유 자원을 관리하기 위한 동기화 비용이 발생합니다.

아이솔레이트 (Isolate):

  • 고립된 메모리: 아이솔레이트는 메모리를 다른 아이솔레이트와 공유하지 않습니다. 각 아이솔레이트는 독립적인 힙 메모리 공간을 가지고 있으며, 데이터는 복사 또는 메시지를 통해 전달됩니다.
  • 메시징 기반 통신: 아이솔레이트 간 데이터는 메시지를 통해서만 교환됩니다. 이는 Dart의 SendPortReceivePort를 사용하여 구현됩니다.
  • 경량 동작: 동기화 비용이 없으므로 멀티스레드보다 간단하고 효율적입니다.

Isolate를 언제 사용할까?

Isolate를 언제 사용해야 하는지에 대한 규칙은 없지만, 다음과 같은 상황에서 Isolate가 유용할 수 있습니다:

  • 매우 큰 JSON 데이터를 파싱하고 디코딩할 때.
  • 사진, 오디오, 비디오를 처리하고 압축할 때.
  • 오디오 및 비디오 파일을 변환할 때.
  • 대규모 목록이나 파일 시스템에서 복잡한 검색 및 필터링을 수행할 때.
  • 데이터베이스와 통신하는 등의 I/O 작업을 수행할 때.
  • 많은 양의 네트워크 요청을 처리할 때.

Isolate의 사용 방법

Isolate는 2가지 방법으로 사용할 수 있습니다.

Simple Worker Isolate 구현

Isolate.run() 함수를 호출하여 Main Isolate에서 New Isolate(Background worker)를 생성하고, main()는 결과를 기다립니다.

const String filename = 'with_keys.json';

void main() async {
  // Read some data.
  final jsonData = await Isolate.run(() async {
    final fileData = await File(filename).readAsString();
    final jsonData = jsonDecode(fileData) as Map<String, dynamic>;
    return jsonData;
  });

  // Use that data.
  print('Number of JSON keys: ${jsonData.length}');
}

Isolate간의 메시지 주고 받기

앞의 Simple Worker Isolate(단기 Isolate)의 경우, 새로운 Isolate를 생성하고 객체를 다른 Isolate로 복사하는데 성능 오버헤드가 발생할 수 있습니다. 코드에서는 Isolate.run을 사용하는 대신에 즉시 종료되지 않는 장기 Isolate을 생성하여 성능을 개선할 수 있습니다.

이를 위해 Isolate.run이 추상화한 몇 가지 격리 관련 API를 사용할 수 있습니다:

  • Isolate.spawn()Isolate.exit()
  • ReceivePortSendPort
  • SendPort.send() 메서드

ReceivePort와 SendPort

장기 Isolate 간의 통신을 설정하려면 두 가지 클래스가 필요합니다 (Isolate 외에도):

  • ReceivePort: 다른 격리로부터 메시지를 처리하는 객체입니다.
  • SendPort: 메시지를 보내는 데 사용됩니다.

포트 설정

새로 생성된 Isolate는 Isolate.spawn 호출을 통해 받은 정보만을 가지고 있습니다. 메인 Isolate와 생성된 Isolate 간에 초기 생성 이후에도 계속해서 통신하려면, 메시지를 주고받을 수 있는 통신 채널을 설정해야 합니다. Isolate들은 메시지 전달을 통해서만 통신할 수 있습니다.

이 양방향 통신을 설정하려면, 먼저 메인 격리에서 ReceivePort를 생성하고, 이를 새로운 격리에서 Isolate.spawn을 사용할 때 SendPort로 전달합니다. 생성된 격리는 자신의 ReceivePort를 만들고, 이를 통해 메인 격리로 SendPort를 다시 보냅니다. 메인 격리는 이 SendPort를 수신하게 되며, 이제 양쪽 모두 메시지를 주고받을 수 있는 개방된 채널을 갖게 됩니다.

  1. Main Isolate에서 ReceivePort를 생성합니다. SendPort는 ReceivePort의 속성으로 자동으로 생성됩니다.
  2. Isolate.spawn()을 사용하여 Worker Isolate를 생성합니다.
  3. Worker Isolate의 첫번째 메시지로 ReceivePort.sendPort의 참조를 전달합니다.
  4. Worker Isolate에서 새로운 ReceivePort를 생성합니다.
  5. Worker Isolate의 ReceivePort.sendPort의 참조를 첫 번째 메시지로 Main Isolate에게 다시 전달합니다.

메시지 주고 받기

  1. Main Isolate의 참조를 사용하여 Worker Isolate의 sendPort로 메시지를 보냅니다.
  2. Worker Isolate의 ReceivePort에 리스너를 설정하여 메시지를 수신하고 처리합니다. 리스너에서 복잡한 계산이나 작업이 실행됩니다.
  3. Worker Isolate에서 Main Isolate의 sendPort를 참조하여 결과를 보냅니다.
  4. Main Isolate의 ReceivePort에 리스너를 설정하여 결과 메시지를 수신합니다.

위 그림들의 포트 설정과 메시지 주고 받기에 대한 예제를 보겠습니다.

import 'dart:isolate';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Isolate Demo',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String _message = 'Result will be displayed here';

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Isolate Demo'),
      ),
      body: Center(
        child: Text(_message),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _startBackgroundTask,
        tooltip: 'Start',
        child: Icon(Icons.play_arrow),
      ),
    );
  }
  
  // 백그라운드 작업을 실행하는 함수
  void _startBackgroundTask() async {
    // 1. Main Isolate의 ReceivePort 생성
    ReceivePort receivePort = ReceivePort();

    // 백그라운드 작업으로부터 메시지를 받습니다.
    receivePort.listen((data) {
      if (data is SendPort) {
        // 6. Worker Isolate의 sendPort를 받으면, 작업 개시 명령어 전달
        data.send('Start work');
      } else if (data is String) {
        // 9. Worker Isolate로부터 받은 데이터를 처리함
        setState(() {
          _message = data;
        });
        // 10. 
        isolate.exit();
        isolate.kill();
      }
    });
    
    // 2. Worker Isolate 생성
    // 3. Main의 ReceivePort.sendPort 참조 전달
    //    sendPort 이외의 정보를 Worker Isolate로 전달 시, params class를 만들어 전달
    Isolate.spawn(_backgroundWork, receivePort.sendPort);
  }
  
  // 백그라운드에서 실행될 작업 - 인자 값은 Main Isolate의 sendPort
  static void _backgroundWork(SendPort sendPort) {
    // 4. Worker Isolate에서 ReceivePort 생성
    final backgroundIsolateStream = ReceivePort();
    
    // 5. Worker Isolate의 ReceivePort.sendPort의 참조를 Main Isolate에게 전달
    sendPort.send(backgroundIsolateStream.sendPort);
    
    await for (final message in backgroundIsolateStream) {
      // 7. Main Isolate의 작업 개시 명령어 수신하면, 작업 시작
      if (message == 'Start work') {
        // Main에서 하기에 복잡한 계산 또는 작업을 수행
        // 뭔가 복잡복잡한... 작업들...
        String result = "Background work completed";
        
        // 8. 계산 결과를 메인 Isolate로 전달
        sendPort.send(result);
      }
    }
  }
}

주석을 보시면, Main과 Worker Isolates간의 메시지를 주고 받는 것을 주석으로 표시했습니다.

다음으론 무엇??

Dart에서의 동시성 (Concurrency in Dart)

Event Loop

Asynchronous programming

  1. Futures
  2. The async-await syntax
  3. Streams
  4. Isolates

참조

https://dart.dev/language/isolates https://dart.dev/language/isolates

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

https://docs.flutter.dev/get-started/flutter-for/dart-swift-concurrency https://docs.flutter.dev/get-started/flutter-for/dart-swift-concurrency

https://velog.io/@ximya_hf/flutter-isolate https://velog.io/@ximya_hf/flutter-isolate

https://lucasblog.kr/flutter-isolate-간단한-설명/ https://lucasblog.kr/flutter-isolate-%EA%B0%84%EB%8B%A8%ED%95%9C-%EC%84%A4%EB%AA%85/

https://kimtaewookdeveloper.tistory.com/41 https://kimtaewookdeveloper.tistory.com/41

https://rlg1133.tistory.com/143 https://rlg1133.tistory.com/143

profile
슬로우플랫폼 기술블로그입니다.

0개의 댓글

관련 채용 정보