Dart 비동기

강정우·2024년 7월 15일
0

Flutter&Dart

목록 보기
83/87
post-thumbnail

Isolate

우선 동기 비동기를 알아보기 전에 Dart 자바 처럼 multi thread 인지 아니면 typescript 처럼 single thread 인지 알아보자.

우선 dart 는 single thread 이나 그러나. 스트리밍, 백그라운드 작업도 가능한데 이는 Dart 는 Isolate 를 기반으로 동작하기 때문이다.

그럼 Isolate 가 뭔지 궁금할 텐데 이건 말 그대로 "독립된" 쓰레드 이다.

멀티 스레드인 자바와 비교하며 설명해주자면

스레드 모델:

멀티스레드: 하나의 프로세스 내에서 여러 개의 스레드가 병렬로 실행.
Isolate: 다트에서는 단일 스레드 모델을 사용하며, Isolate는 독립적인 실행 환경을 제공.

메모리 공유:

멀티스레드: 스레드 간에 메모리 공유.
Isolate: Isolate 간에는 메모리 공유 X. 각 Isolate는 자신의 메모리 공간을 가짐.

통신 방식:

멀티스레드: 스레드 간 직접 메모리 접근으로 통신.
Isolate: Isolate 간 통신은 메시지 전송 방식을 사용.

메모리 안전성:

자바: 경우 멀티스레드 프로그래밍에서 공유 메모리에 대한 동기화 문제가 발생할 수 있음.
다트: Isolate 간 메모리를 공유하지 않으므로, 이런 문제가 발생 X => 대신 각 Isolate 와 메시징으로 데이터를 교환

병렬 처리:

자바: 멀티스레드를 통해 병렬 처리를 할 수 있음.
다트: Isolate를 사용하여 병렬 처리를 할 수 있음. 각 Isolate는 독립적으로 실행되므로, CPU 코어를 효율적으로 활용할 수 있음.

확장성:

자바: 스레드 수가 많아지면 오버헤드가 증가하여 성능이 저하될 수 있음.
다트: Isolate를 추가하여 확장성을 높일 수 있음. Isolate 간 메모리 공유가 없음.

실행 환경 격리:

자바: 멀티스레드 간 실행 환경이 공유됨.
다트: Isolate는 독립적인 실행 환경을 가지므로, 한 Isolate에서 문제가 발생해도 다른 Isolate에 영향을 미치지 않음.

정리하자면 다트가 갖고 있는 특이한 Isolate 방식은 메모리 안전성, 병렬 처리, 확성정이 있어 굉장히 효율과 성능이 좋다.

사용법

_isolate = await Isolate.spawn<SendPort>(
    sleepApp, _receivePort.sendPort);

대충 이렇게 사용한다. 예제를 봐보자.

  String? loadedData;
  late Isolate _isolate;
  final _receivePort = ReceivePort();

  /// 리스너를 등록
  /// _receivePort.sendPort를 인자로 전달해줘서 등록해준 리스너에 응답을 줄 수 있도록 함.
  
  void initState() {
    super.initState();
    _receivePort.listen((message) {
      setState(() {
        loadedData = message.toString();
      });
    });
  }

  
  Widget build(BuildContext context) {
    return MainFrame(
      route: FeatureEnum.isolate,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          Builder(builder: (_) {
            if(loadedData == null){
              return const SizedBox();
            }
            if(loadedData!.isEmpty){
              return const CircularProgressIndicator();
            }
            return Text(loadedData!);
          },),
          const SizedBox(height: 10,),
          ElevatedButton(onPressed: onTapMethodCall, child: const Text('Call Method')),
          const SizedBox(height: 10,),
          ElevatedButton(onPressed: onTapMethodCallWithIsolate, child: const Text('Call Method With Isolate')),
          const SizedBox(height: 10,),
          ElevatedButton(onPressed: onTapCancelIsolate, child: const Text('Cancel Isolate')),
        ],
      ),
    );
  }

이런 코드가 있다고 가정할 때

/// 아이솔레이트 없이 sleep을 호출함 -> 화면 멈춤
  Future<void> onTapMethodCall() async {
    setState(() {
      loadedData = '';
    });
    sleep(const Duration(milliseconds: 5000));
    setState(() {
      loadedData = 'just sleep over';
    });
  }

  /// 아이솔레이트를 통해 별도 메모리를 할당시켜 UI에 영향을 주지 않고 로직 실행
  Future<void> onTapMethodCallWithIsolate() async {
    setState(() {
      loadedData = '';
    });

    _isolate = await Isolate.spawn<SendPort>(
        sleepApp, _receivePort.sendPort);
  }

  /// 작업 중단
  void onTapCancelIsolate(){
    _isolate.kill(priority: Isolate.immediate);

    setState(() {
      loadedData = 'Task canceled';
    });
  }
  
  void sleepApp(SendPort sendPort) {
    sleep(const Duration(seconds: 5));
    sendPort.send("Completion");
  }

각각의 함수는 이렇게 생겼다고 하자.

onTapMethodCall 를 실행하면 1개의 thread 에서 sleep 과 렌더링을 모두 실행하므로 앱이 멈춰버린다.
대신 onTapMethodCallWithIsolate 를 사용하면 Isolate(격리된 메모리 공간)가 따로 돌아서 sleep 따로 rendering 따로 돌아간다.

주의
Isolate.spawn()에 등록해줄 함수는 Top-Level의 함수이거나 static 함수여야 한다는 점이다.

동기

async / await / Future
통상 1 회만 응답을 돌려 받는 경우.

void somethingFn () {
	print("TODO Done in right away");
}

Future<void> somethingFn (int second) async {
	await Future.delayed(Duration(seconds: second));
    print("TODO Done in $second seconds");
}

비동기

async* / yield / Stream
지속적으로 응답을 돌려 받는 경우.

Stream<int> somethingFn () async* {
	int counter = 0;
    while (counter <= 10) {
    	counter++;
        await Future.delayed(Duration(seconds: 1));
        print("TODO is running $counter");
        yield counter;
    }
    
    print("TODO is Done");
}

somethingFn().listen((event){});

yield 는 뭔가 Stream 이 동작하는 과정에서 중간중간 당시 값을 return 해준다 라고 생각하면 되겠다.
통상 Stream Listener 나 Stream Builder 를 일반적으로 사용하는데 여기서는 그냥 간단한 예제로 대체하겠다.

참고로 Stream 은 청취하고 있는 객체가 없으면 바로 종료된다.

reference

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

profile
智(지)! 德(덕)! 體(체)!

0개의 댓글