Skip to content

Instantly share code, notes, and snippets.

@muabe
Last active June 13, 2022 07:40
Show Gist options
  • Save muabe/3afb3967892972c69fd8b3e82e4ac584 to your computer and use it in GitHub Desktop.
Save muabe/3afb3967892972c69fd8b3e82e4ac584 to your computer and use it in GitHub Desktop.

Dart 비동기 Future

비동기는 모바일 개발에 있어서 많은 에러의 원인이 되며 어플리케이션의 심각한 문제를 초래하기도 한다. 따라서 비동기에 대한 정확한 이해와 안전한 사용 방법을 익히는 것이 중요하다. Dart의 공식문서와 소스코드를 보며 좀더 자세하게 이해하려 노력했다.

Future 기본 사용법

기본적으로 비동기로 실행하는 방법으로, Future 생성자에 함수를 넣어 Runnable 형태로 실행할 수 있다.

Runnable

print("ready");

Future((){
    print('future');
});

print("end");
/*출력
ready
end
future
*/

Future 생성자에 함수를 넣을 수 있음으로 다음과 같이 함수로 나누어 비동기를 실행할 수 있다.

void main(){
    print("ready");
    Future(future);
    print("end");
}
void future(){
    print('future');
}

/*출력
ready
end
future
*/

## 비동기로 작업이 다 끝났을때 then으로 처리된 결과의 callback을 받을 수 있다.

### callable
```dart
Future<int>((){
    print('future');
    return 10;
}).then((value) => print('complete:$value'));

함수형

Runnable 함수형. 리턴타입 void. Future.value를 리턴 해야함 callable 함수형, 리턴타입이 있을때. Future.value(data) 값을 넣어서 리턴 해야함

Future<int> future(){
  print('**future**'); // 비동기 영역 아님
  return Future.value(10);
}

void main() {
  print('ready');
  future().then((value) => print('complete:$value'));
  print('end');
}
/* 출력
ready
**future**
end
complete:10
*/

Note : 함수안에 일부만 비동기, 정확하게 일반코드는 동기, Future만 비동기

비동기 실행 순서

Thread인가?

print('ready');
for(int i=0;i<1000; i++){
    Future.delayed(Duration(seconds: 3)).then((value) => print(i));
}
print('end');

타이밍

나중에 실행되는 느낌. 이벤트 큐에 넣기 때문에 순서대로 나중에 실행됨.

print('ready');
Future(() => print("future1"));
Future(() => print("future2"));
Future(() => print("future3"));
print('end');
print('-----------------');
  
/* 출력
ready
end
-----------------
future1
future2
future3
*/

의미없는 함수형

Future<int> future(){
  return Future<int>((){
    print('future');
    return 10;
  });
}

void main() {
  print('ready');
  future().then((value) => print('complete:$value'));
  print('end');
}

/* 출력
ready
end
**future**
complete:10
*/

타이밍 생각해보기

Future<void> future(){
  print('future ready');
  Future.value().then((value) => print('future.value'));
  print('future end');
  return Future.value();
}

void main() {
  print('main ready');
  future().then((value) => print('complete'));
  print('main end');
}

/* 출력
main ready
future ready
future end
main end
future.value
complete
*/

block과 async/await

함수영 지원을 위함 async키워드. block과 이벤트 큐에 등록을 위한 await키워드. async와 await가 항상 짝을 이루어야함. 함수형으로만


Future 심화

Dart의 비동기는 다른 언어와 다른게 사용한다. 비동기를 알아보기 전에 먼저 동기화는 어떻게 하는지 코드를 보자

  //java 동기화 코드(기본적으로 동기화)
  Thread.sleep(3000);
  
  //dart 동기화 코드
  await Future.delayed(Duration(seconds: 3));

위 처럼 java와 다르게 Dart에서는 기본적으로 비동기가 우선적으로 적용되고 block(await)를 하여 동기화 하는것을 볼수 있다.

이번엔 비동기화 코드를 보자

  //java 비동기화 코드
  new Thread(new Runnable(){
    public void run(){
      Thread.sleep(3000);
    }
  )}.start();
  
  //dart 비동기화 코드
  Future.delayed(Duration(seconds: 3));

Dart는 Future클래스로 비동기를 지원하고 다른 언어와 비교하여 많은 부분이 다른데 그로인해 몇가지 궁금점들이 생긴다.

  • Thread.sleep 같이 다른 언어들은 동기화가 기본인데 dart는 거꾸로 비동기를 기본으로 사용하는가?
  • Future 클래스는 왜 만들어진 것인가?
  • Future의 많은 생성자와 Factory 패턴은 왜 필요한 것인가?
  • 스레드없이 비동기는 어떻게 지원하는가?

Dart의 Future 클래스와 Event loop의 연관성을 살펴보면서 궁금증을 하나씩 짚어 보도록 하자

Event loop, MacroTask(scheduleMicrotask)

Dart는 비동기를 위해서 Thread를 생성하지 않는다. Event loop의 이벤트 큐에 콜백을 등록하여 vm에서 비동기를 실행해준다. Dart에서 Event loop에 이벤트 등록과 콜백 처리는 다행히 내부적으로 알아서 처리해 주고 있다.
아래는 Event loop의 동작 방식에 대한 참고 자료이다.

Note : Dart는 Thread를 사용하지 않고 Event loop로 비동기를 지원한다.


Future

Future의 동작 방식을 설명하는 곳은 많은데 실제 Future의 코드적인 개념 즉, 코드적인 관점에서 어떻게 생성하고 사용하는지 어떤것을 주의하여야 하는지 알려주는 곳이 없었다. Future는 비동기를 실행하기 위한 operation이라 생각하면 좋다.
비동기를 실행할 수 있는 방법은 크게 두가지다(예외적인 케이스들도 있다)

  • Futrue 객체를 생성하는 방법
  • async/await를 사용하는 방법(나중에 살펴봄)

Futrue는 비동기를 이벤트 루프에 등록하는 콜백의 작업 단위가 된다.
이벤트 루프가 이벤트큐를 통해 비동기 작업을 실행해 주는데 Future가 이벤트 큐에 콜백을 등록해주는 인터페이스 역할을 한다. 이벤트 루프는 Dart 내부적으로 알아서 동작하기 때문에 신경쓸 필요는 없지만 Future를 통해서 이벤트큐에 콜백을 등록하고 실행 한다는건 잊지 말아야 한다.
기본적으로 Future를 이용해서 비동기를 어떻게 실행하는지 보자.

  print('ready');
  
  int count = 3;
  for(int c=0; c<count; c++) {
    for (int i = 0; i < 1000000; i++) {
      for (int j = 0; j < 10000; j++) {
        100000 * 100;
      }
    }
    print("count:$c");
  }

  print('end');

카운트 하는 작업이 오래걸려 비동기로 실행하고 싶다면 Future 기본 생성자를 이용하여 비동기를 실행할 수 있다.

  print('ready');

  //Future 생성자를 이용하면 비동기로 실행된다.
  Future(() {
    int count = 3;
    for (int c = 0; c < count; c++) {
      for (int i = 0; i < 1000000; i++) {
        for (int j = 0; j < 10000; j++) {
          100000 * 100;
        }
      }
      print("count:$c");
    }
  });

  print('end');

Note : Future클래스는 비동기를 실행하는 주목적과, 비동의 상태를(Completed, UnCompleted) 모니터링하고 에러나, 완료, 취소 등등 비동기를 컨트롤 할수 있는 콜백과 기능들을 지원하는 2가지 목적을 가지고 있다.


Future의 생성자와 팩토리

비동기를 실행하려면 Future 클래스의 생성자 및 팩토리를 이용하여 비동기를 실행할 수 있다. 위에 예제처럼 Future 기본 생성자로 비동기를 실행할 수 있으며 비동기 작업에 관련된 팩토리들이 있다. Future.delay가 팩토리중 하나이다. 그런데 Timer 클래스가 있는데 Future.delay 팩토리는 왜 또 있는걸까? 이밖에도 왜 많은 팩토리들을 만든 걸까?

//Timer클래스로 3초가 delay를 줄수 있다.
Timer(Duration(seconds: 3), (){print('Timer');});

//Timer클래서와 동일한 역할을 하는 Future.delayed
Future.delayed(Duration(seconds: 3), (){print('Future.delayed');});

Timer클래스도 자체적으로 비동기로 실행되며 지연 시간만큼 기다렸다 콜백함수를 호출해준다. Future.delay의 내부 소스코드를 보면 Timer 클래스를 감싸서 만들었다. 여기서 눈여겨 봐야할 것은 굳이 Timer클래스를 감싸서 만든 이유이다. 비동기로 특정 시간만큼 지연한 후 작업을 실행만 하는 목적이라면 Future.delay와 다를바 없다.(당연히 Timer를 감싸서 만들었기에) 하지만 에러를 처리한다 던지 이후 추가작업이 필요하다면 어떨까?

  Future.delayed(Duration(seconds: 3), () => print("job finsh"))
      .onError((error, stackTrace) => print("error"))
      .then((value) => print("after job"));

Timer클래스와 달리 Future.delayed는 에러처리 및 완료처리 등을 할수 있으며 부가적으로 비동기의 상태(commpleted/uncompleted) 모니터링 할수 있다. 다시 말해 Timer를 Future는 감싸면서 비동기를 통일된 인터페이스로 처리할 수 있게 해준다. 그래서 내부적으로 팩토리들을 만들어 모든 비동기들을 Future라는 인터페이스로 통일하여 사용할 수 있게 해준다.

Future의 목적과 생성자와 팩토리의 필요성

한가지 중요한 사실은 Dart는 Thread가 아닌 Event loop를 사용하기에 이벤트큐에 콜백을 등록 해줘야한다. 이는 매우 복잡한 소스코드로 이루어져 있다. Future가 이벤트큐 등록을 대신 해줌으로써 쉽게 비동기를 사용할 수 있다.
이벤트큐에 등록이 복잡하다는 것은 동기화코드를 비동기로 변환하기 어렵다는 것이다. 하지만 await를 사용하여 비동기를 동기화 코드로 만들기는 쉽다. 그래서 Future는 어려운 비동기 변환코드를 많은 생성자와 팩토리로 미리 만들어 놓은 것이다.
Future를 이용해서 쉽게 비동기화를 하고 await로 쉽게 동기화 할수 있다. 이러한 이유로 다른 언어와 달리 비동기를 거꾸로 기본으로 사용한다.

  await Future.delayed(Duration(seconds: 3));

Note: dart에서 비동기로 만드는것은 어렵지만 await를 사용해 동기화로 만드는건 쉽다.

이모든 일들을 Future클래스 하나로 인터페이스 삼아 모든 일들을 수행한다.

Future의 역할

  • 주목적으로 비동기를 실행해줌(정확하게는 이벤트큐에 콜백을 등록)
  • 상태 모니터링과 에러처리 등 비동기에 필요한 기능 지원
  • 어려운 비동기 코드를 쉽게 할수 있게 생성자와 팩토리를 지원 해줌
  • 모든 비동기화를 동일하게 하나의 인터페이스로 제어할 수 있게함

async/await

Future 기본 생성자는 비동기로 실행할 함수를 받는다. 함수를 매개변수로 받는다는 것은 비동기 함수를 만들 수 있다는 것이다.
따라서 함수를 따로 정의해서 아래처럼 사용할 수 있다.

void main() async{
  print("ready");
  Future(value);
  print("end");
}

void value(){
  print("value");
}

/*출력
 ready
 end
 value
 */

이처럼 함수를 Future 생성자에 넣어 비동기로 실행할 수 있다.
함수에 aync를 붙여 !!!!!

비동기 코드를 함수로 분리하여 사용하는 경우 async를 사용하여 Future를 대신할 수 있다. 이는 async는 코드의 간견함과 가독성, 그리고 비동기로 Functional language 쉽게 지원할 수 있게 만들수 있는 키워드 이다.

  Future<void> counter async{
  int count = 3;
    for (int c = 0; c < count; c++) {
      for (int i = 0; i < 1000000; i++) {
        for (int j = 0; j < 10000; j++) {
          100000 * 100;
        }
      }
      print("count:$c");
    }
  }

counter();  

위 코드를 실행하면 비동기로 실행이 안된다 왜 안되는걸까? 아래 제대로 동작하는 코드를 보자

Future<void> sleep() async{
  await Future.delayed(Duration(seconds: 2),()=>print("sleep"));
}

sleep();

위 두개의 예제의 차이는 await가 있고 없고의 차이이다. 다시말해 async 블록 안에 await가 반드시 있어야 비동기가 동작한다. 내부적으로 async블록안에 await를 구문부터 이벤트큐에 콜백으로 등록되기 때문이다.
아래는 Dart공식문서의 await에 대한 주의 사항이다.(https://dart.dev/guides/language/language-tour#asynchrony-support)

Note: Although an async function might perform time-consuming operations, it doesn’t wait for those operations. Instead, the async function executes only until it encounters its first await expression (details). Then it returns a Future object, resuming execution only after the await expression completes.

async 키워드를 사용한다면 블럭 첫마디에 await로 시작하는 것이 좋다. 하지만 await는 Future를 리턴하는 함수에만 붙일 수 있기 때문에 어려움이 있다. await를 추가하는 가장 쉬운 방법은 아래와 같다.

Future<void> counter async{
  await null;
  int count = 3;
    for (int c = 0; c < count; c++) {
      for (int i = 0; i < 1000000; i++) {
        for (int j = 0; j < 10000; j++) {
          100000 * 100;
        }
      }
      print("count:$c");
    }
  }

await null 을 첫 시작코드에 넣음으로써 비동기가 실행되는것을 볼수 었다. 사실 await null은 이벤트큐에 넣어지는 것이 아니다. Event loop의 macrotask 영영에 추가된다. macrotask는 이벤트큐 보다 먼저 선행되기 때문데 더 빠르다. await null 사용하지 않는다면 코드를 Future로 리턴하는 함수로 감싸서 만들거나 Completer를 사용하여야 한다.

아래는 Completer와 Future로 만드는 몇가지 방법을 소개합니다.

Completer

Future<List<Base>> GetItemList(){
  var completer = new Completer<List<Base>>();
    
  // At some time you need to complete the future:
  completer.complete(new List<Base>());
    
  return completer.future;
}

```dart
Future<int> cubed(int a) async {
  final completer = Completer();
  if (a < 0) {
    completer.completeError(ArgumentError("'a' must be positive."));
  } else {
    completer.complete(a * a * a);
  }
  return completer.future;
}

Future() constructor

Future 생자자를 이용하여 함수를 Future로 만드는 방법

Future<int> cubed(int a) {
  return Future(() => 100 * 10);
}

Future named constructor : Future.value, Future.error

Future가 특정 값을 리턴해야한다면 Future.value, Future.error 사용하여 리턴할 수 있다.

Future<int> cubed(int a) {
  if (a < 0) {
    return Future.error(ArgumentError("'a' must be positive."));
  }
  return Future.value(10*100);
}

비동기의 실행 순서

Future를 실행하면 마치 비동기로 실행되는것 같다. 다른 코드들이 먼저 실행되고 Future의 비동기 코드가 실행되기 때문이다.
하지만 연산만으로 이루어진 코드들을 실행해보면 비동기로 실행되지 않는 것을 볼수 있다.

void delay(int count , String msg){
  for (int c = 1; c <= count; c++) {
    for (int i = 0; i < 100000; i++) {
      for (int j = 0; j < 15000; j++) {
        100000 * 100;
      }
    }
    print('$msg:$c');
  }
}

Future<void> futureDelay({String msg = 'future'}) async{
  await Future((){
    delay(3, msg);
  });
}

void main() async{
  print('ready');
  futureDelay(msg:'future1'); //1번
  futureDelay(msg:'future2'); //2번
  delay(3, 'code'); //3번
  print('end');
}

//출력
//ready
//code:1
//code:2
//code:3
//end
//future1:1
//future1:2
//future1:3
//future2:1
//future2:2
//future2:3

만약 Future가 말그대로 비동기였다면 print가 위와 같이 균일하게 나오지 않았을 것이다. 어떠한 순서를 가지고 순차적으로 실행하고 있다.
3번 -> 1번 -> 2번
이것으로 알수 있는 것이 있다. 먼저 같은 레벨의 코드블럭에 코드들을 먼저 실행하고 이후 Future코드가 이벤트 큐에 들어간 순서대로 실행된다.

  • 1.코드블럭 실행
  • 2.Future 이벤트큐에 등록된 순서대로 순차 실행

결론적으로 연산코드는 비동기로 실행되지 않는다. Future를 순수하게 Thread와 같은 비동기로 생각하면 안된다. 프로그램이 block이 걸려 실행이 느려질수 있기 때문이다.
물론 Time delay 등 이벤트를 받는 부분에서 보면 비동기와 같을 것이다. 이것은 단지 연산이 없이 대기하는 옵저버 형태일때 이다. 연산코드를 비동기처럼 사용할 수 없다. 'event loop' 말그대로 이벤트를 받을때 까지 기다렸다가 notify를 주는 구독형태라 말할수 있다. Future는 시간 delay, 사용자 입력 대기 등 옵저버 형태로 사용하는것이 현재로써 올바를 사용방법인것 같다.

Note : Future는 완전한 비동기가 아니다. 옵저버 형태로 사용하는 것이 좋고 연산코드가 있다면 isolate를 사용하길 권장한다.


FutureBuilder

...

FutureProvider

...

FutureOr 아래부터는 미정리

https://stackoverflow.com/questions/18423691/dart-how-to-create-a-future-to-return-in-your-own-functions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment