Foggy day

[Dart] Isolate 사용법(Thread 작업) 본문

Flutter/Dart 문법

[Dart] Isolate 사용법(Thread 작업)

jinhan38 2024. 5. 9. 18:40

 

이번 포스팅에서는 Dart의 Isolate에 대해 알아보겠습니다.

 

Isoate는 별도의 Thread에서 작업을 할 수 있도록 도와주는 클래스입니다. 시간이 오래 걸리는 작업을 하면서 화면은 계속해서 업데이트 하고 싶은 경우 사용하면 좋습니다.

 

 

 

 

1. 메인 스레드에서 작업

2. Isolate.run

3. Isolate.spawn

 

 

1.  메인 스레드에서 작업

무거운 작업을 메인스레드에서 하게 되면 화면이 멈추는 현상이 발생합니다. 왜냐하면 Flutter는 기본적으로 단일 스레드를 사용하기 때문입니다. UI를 그리는 메인 스레드에서 무거운 연산 작업을 하게 될 경우 화면을 계속해서 업데이트할 수 없기 때문에 멈추는 것입니다. 이러한 것을 정크(jank)라고 합니다. 

아래 메인 스레드에서 while문을 돌리는 예제입니다. sleep을 추가해 놨기 때문에 잠시동안 화면이 멈춰버립니다. 이러한 문제를 해결하기 위해서 Isolate를 사용할 수 있습니다.

int count = 0;

/// 메인 스레드에서 작업 실행
ElevatedButton(
  onPressed: () {
    setState(() {
      count = 0;
    });
    while (count < 300) {
      count++;
      sleep(const Duration(milliseconds: 10));
      setState(() {});
    }
  },
  child: const Text("Main Thread"),
)

 

영상을 보면 작업이 완료 될 때까지 프로그레스바가 멈춰 있는 것을 볼 수 있습니다. 

 

 

 

 

 

 

2.  Isolate.run

Isolate.run을 사용하면 다른 스레드에서 작업을 요청한 후 결과 값을 돌려받을 수 있습니다. 마치 Future 함수의 형태와 유사합니다.

이때 주의할 것은 Isolate.run의 구현부에서 메인스레드에 있는 변수를 참조하거나 setState를 호출할 경우 오류가 발생합니다. Isolate는 다른 메모리이기 때문에 메인스레드의 메모리에 있는 것들을 공유할 수가 없기 때문입니다. 이러한 문제를 해결하기 위한 것이 Isolate.spawn입니다. 

  /// isolate.run 을 사용해서 작업 실행
  ElevatedButton(
    onPressed: () => isolateRun(),
    child: const Text("Isolate run"),
  )
  
  ....

  /// Isolate.run 을 사용해서 다른 스레드 사용
  /// setState 나 다른 변수들을 사용하면 오류 발생 -> 메모리를 공유하지 않기 때문에 변수도 공유할 수 없음
  /// 메인 스레드와 Isolate 스레드 간에 통신을 하기 위해서는 Isolate.spawn 사용 필요.
  void isolateRun() async {
    setState(() {
      count = 0;
    });
    var result = await Isolate.run(() {
      int isolateCount = 0;
      while (isolateCount < 300) {
        isolateCount++;
        sleep(const Duration(milliseconds: 10));
      }
      return isolateCount.toString();
    });
    setState(() {
      count = int.parse(result);
    });
  }

 

영상을 보면 작업이 완료 될 때까지 프로그레스바가 계속 돌아가고 있는 것을 볼 수 있습니다. 

 

 

 

 

 

 

 

3.  Isolate.spawn

Isolate.spawn을 사용하면 Isolate 스레드와 메인 스레드가 통신할 수 있습니다. 마치 Stream 함수의 형태와 유사합니다.

spawn에서는 run과 마찬가지로 구현부가 필요하고, 두 스레드를 연결시켜 줄 도구가 필요합니다. 도구의 역할을 하는 것이 ReceivePort입니다. ReceivePort는 Stream 클래스를 구현한(implement)한 클래스입니다. 때문에 StreamSubscription를 return하는 listen 함수를 사용할 수 있습니다. spawn의 구현부에서 sendPort.send() 함수로 데이터를 보내면 listen 함수에서 메인스레드로 데이터를 받을 수가 있습니다. 

  /// isolate spawn 을 사용해서 작업 실행
  ElevatedButton(
    onPressed: () => isolateSpawn(),
    child: const Text("Isolate spawn"),
  ),
  
  ....

  void isolateSpawn() {
    setState(() {
      count = 0;
    });

    /// 메인 스레드와 isolate 스레드를 연결 시켜줄 ReceivePort 생성
    ReceivePort receivePort = ReceivePort();

    /// receivePort.listen을 사용해서 Isolate가 전달하는 데이터를 메인 스레드에서 수신
    receivePort.listen((data) {
      count = int.parse(data);
      setState(() {});
    });

    /// Isolate 생성
    Isolate.spawn(
      /// 다른 스레드에서 작업할 내용
      /// sendPort.send() 함수를 사용해서 메인 스레드에게 데이터를 전달할 수 있음
      (sendPort) async {
        int isolateCount = 0;
        while (isolateCount < 300) {
          isolateCount++;
          sendPort.send(isolateCount.toString());
          sleep(const Duration(milliseconds: 10));
        }
      },

      /// 앞서 생성한 sendPort 전달
      receivePort.sendPort,
    );
  }

 

영상을 보면 실시간으로 count 값이 변경되는 것을 볼 수 있습니다.

 

 

 

*만약에 Isolate.spawn에 메소드를 사용할 경우 때 첫 번째 인자에 함수를 넣고 싶다면 함수는 반드시 static이어야 합니다. Isolate는 다른 메모리에서 실행되기 때문에 해당 함수가 어떤 객체의 상태에도 의존하지 않아야 합니다. 다시 말해 독립적인 함수의 형태가 필요합니다.

 

 

 

 

 

 

Full code

import 'dart:io';
import 'dart:isolate';

import 'package:flutter/material.dart';

class Home extends StatefulWidget {
  const Home({super.key});

  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            /// 메인 스레드에서 작업 실행
            ElevatedButton(
              onPressed: () {
                setState(() {
                  count = 0;
                });
                while (count < 300) {
                  count++;
                  sleep(const Duration(milliseconds: 10));
                  setState(() {});
                }
              },
              child: const Text("Main Thread"),
            ),

            /// isolate.run 을 사용해서 작업 실행
            ElevatedButton(
              onPressed: () => isolateRun(),
              child: const Text("Isolate run"),
            ),

            /// isolate spawn 을 사용해서 작업 실행
            ElevatedButton(
              onPressed: () => isolateSpawn(),
              child: const Text("Isolate spawn"),
            ),

            Text(
              "count : $count",
              style: const TextStyle(fontSize: 25),
            ),
            const Center(child: CircularProgressIndicator()),
          ],
        ),
      ),
    );
  }

  /// Isolate.run 을 사용해서 다른 스레드 사용
  /// setState 나 다른 변수들을 사용하면 오류 발생 -> 메모리를 공유하지 않기 때문에 변수도 공유할 수 없음
  /// 메인 스레드와 Isolate 스레드 간에 통신을 하기 위해서는 Isolate.spawn 사용 필요.
  void isolateRun() async {
    setState(() {
      count = 0;
    });
    var result = await Isolate.run(() {
      int isolateCount = 0;
      while (isolateCount < 300) {
        isolateCount++;
        sleep(const Duration(milliseconds: 10));
      }
      return isolateCount.toString();
    });
    setState(() {
      count = int.parse(result);
    });
  }

  void isolateSpawn() {
    setState(() {
      count = 0;
    });

    /// 메인 스레드와 isolate 스레드를 연결 시켜줄 ReceivePort 생성
    ReceivePort receivePort = ReceivePort();

    /// receivePort.listen을 사용해서 Isolate가 전달하는 데이터를 메인 스레드에서 수신
    receivePort.listen((data) {
      count = int.parse(data);
      setState(() {});
    });

    /// Isolate 생성
    Isolate.spawn(
      /// 다른 스레드에서 작업할 내용
      /// sendPort.send() 함수를 사용해서 메인 스레드에게 데이터를 전달할 수 있음
      (sendPort) async {
        int isolateCount = 0;
        while (isolateCount < 300) {
          isolateCount++;
          sendPort.send(isolateCount.toString());
          sleep(const Duration(milliseconds: 10));
        }
      },

      /// 앞서 생성한 sendPort 전달
      receivePort.sendPort,
    );
  }
}

 

 

 

참조 문서

https://dart.dev/language/isolates