How to become a real programmer/Front-End

Flutter - Dart기초(4) - Asynchronous Programming

MinDDokDDok 2022. 12. 30. 00:44

동기 프로그래밍 Synchronous Programming? / 비동기 프로그래밍 Asynchronous Programming?

동기 프로그래밍은 즉, 하나의 프로세스를 수행하다가 다른 프로세스의 요청이 들어가게 되면 CPU의 작동이 멈추고 해당 요청을 받는 프로세스로 이동하게 된다. 즉 CPU가 가지고 있던 프로세스와 받는 프로세스 간 동기화가 되어있는 것이다. 

뭐 현재는 컴퓨터 속도가 빨라졌기 때문에 느리다는 체감을 받기는 힘들지만, 만약 서버요청과 같은 네트워크 상 어쩔 수 없이 시간이 걸리는 경우에는 빠른 컴퓨터 속도와는 다르게 시간이 오래걸려 사용자 경험이 크게 저해될 수 있다.

 

이를 방지하기 위해 비동기 프로그래밍은 어떠한 요청을 전달하면 바로 CPU를 보내는 것이 아닌 해당 요청에 대한 repsonse가 도착할 때 까지 기존 프로세스가 계속 사용한다. 혹은 사용중인 프로세스가 서버요청을 날리면 다른 프로세스가 CPU를 전달받아 사용한다.

 

1. Future - Asynchronous Programming을 가능케 해주는 키워드

Future.dalayed로 첫 번째 인자로는 지연시간, 두 번째는 실행 함수를 던져주면 결과로 우선 함수시작이 나타난 후, 2초 정도 흐르고 'Delay 끝'이 출력되는 것을 볼 수 있다.

void main(){
  Future<String> name = Future.value('승환');
  Future<int> number = Future.value(1);
  Future<bool> isTrue = Future.value(true);
  
  // 2개의 파라미터
  // 1번 파라미터 - 지연할 기간(얼마나 지연할 것인지) duration
  // 2번 파라미터 - 지연 시간이 지난 후 실행할 함수
 
  print('함수시작');
  
  Future.delayed(Duration(seconds: 2), (){
    print('Dalay 끝');
  });
  
  }

asynchronous의 증명으로 아래 코드를 실행해보면,

void main(){
  Future<String> name = Future.value('승환');
  Future<int> number = Future.value(1);
  Future<bool> isTrue = Future.value(true);
  
  
  }

void addNumbers(int number1, int number2){
  print('계산 시작 : $number1 + $number2');
  
  // 서버 시뮬레이션
  
  Future.delayed(Duration(seconds: 2), (){
    print('계산 완료 : $number1 + $number2 = ${number1 + number2}');  
});
  print('함수 완료');
  }

결과는,

계산시작 출력, 함수완료를 먼저 출력 후 계산완료를 출력한다.

이유는 Synchronous라면 당연히 컴퓨터가 자유의지를 마침내 지니게 되어 인간을 파괴시키려는 생각이 아니라면 계산시작 후 2초 후 계산완료를 띄우고 함수완료를 띄울 테지만 Asynchronous 이기에 cpu가 함수완료를 먼저 실행 후 시간이 다 된 계산완료를 띄우는 것이다

 

2. Await - 절차에 맞는 Asynchronous Programming 

사용하려는 함수 부분에 async, 그리고 내부 명령어에서 await을 써주면 해당 부분을 기다린다(await는 Future 자료형에만 사용가능한 키워드이다). 즉 계산시작 - 계산완료 - 함수완료 순으로 출력가능하다는 뜻이다.

하지만 이 동안 cpu가 쉬는 것은 아니고 다른 명령을 수행하는데, 이를 증명하기 위해 아래 코드를 보자.

원래 같았으면, 계산시작1 - 함수완료1  - 계산시작2 - 함수완료2 - 계산완료1 - 계산완료2 순으로 출력되었지만,

위를 보면 절차대로 수행되고 있음을 알 수 있고, 그렇다고 Synchronous하게 동작하지도 않기에(cpu 권한을 계속 가지고 있지 않기에) addNumbers(1, 2)의 다음 명령인 addNumbers(2, 4)를 또 수행하는 것을 볼 수 있다. 만약 Synchronous 했다면 계산시작 1 - 계산완료1 - 함수완료 1 - 계산시작 2 - 계산완료 2 - 함수완료 2 순이었을 것이다

 

즉 , await를 활용해 async 코드를 논리적으로 기다릴 수 있다는 것! 그렇다고 그 동안 CPU가 노는 것도 아니고 다른 것을 수행할 수 있다는 것

 

그래도 두 개 동시에 하는건 출력 시 헷갈릴 수 있기에 addNumbers도 await 해주자. 그러기 위해선 해당 함수를 Future 자료형으로 변환해야한다. 그리고 main 함수에 async를 달아준 후, 코드를 수행하면

차례대로 잘 된다. 물론 대기하는 2초동안 cpu가 저렇게 멈춰있는 상태가 아니라 await 말고도 다른 함수가 있다면, 해당 함수를 실행할 것이다.

 

2. Stream -  Asynchronous Programming을 가능케 해주는 키워드2(최근 사용빈도 높아짐)

Stream은 Future와 거의 유사한데 차이점은, Future은 순간에 여러번 값을 받아내기 위해선 함수를 여러번 수행하는 방법밖에 없다. Stream은 yield라는 명령어를 통해서 Stream 닫을 때 까지 계속 무한에 가깝게 값을 받을 수 있다. 즉 어느 한순간에 값을 한번만 받는게 아닌 지속적으로 받을 수 있다는 것. 그렇기에 복잡하다 ㅎㅎㅋ;

import 'dart:async';

void main() {
  final controller = StreamController();
  final stream = controller.stream;
  
  final streamListener1 = stream.listen((val){
    print('Listener 1 : $val');
  });
  // 함수를 받음 - 즉 함수가 값을 listen할 때 값이 들어오면 함수가 시행
  
  controller.sink.add(1);
  controller.sink.add(2);
  controller.sink.add(3);
  controller.sink.add(4);
  controller.sink.add(5);
  
}

stream 사용을 위해선 dart:async 라이브러리를 import해오고 선언해준 후 사용해줘야 한다.

streamListener1이라는 함수를 만들고 함수를 받을 때 값이 들어오면 함수가 시행되도록 해주면 우선 리스닝한 1, 2, 3, 4, 5를 붙인 문자열을 각각 출력한다

 

Stream을 여러번 Listening 하는 방법?

다른 리스너를 만들어주면 된다, 다만 controller.stream은 기본적으로 하나의 리스너만 제공하기에 메소드로 asBroadcaseStream()을 써줘야 한다.

그 결과, 값 추가와 함께 함수가 줄줄 잘 시행되는 것을 볼 수 있다.

 

즉석해서 스트림값을 변경시키는 방법?

짝수 값을 입력받을 땐 리스너 1, 홀수 값일 땐 리스너 2를 출력한다면 where을 써주면 된다

이렇게 이전에 알고 있던 함수들을 활용해 결과값들을 출력할 수 있다

 

함수로 Stream을 제공하는 방법?

함수 앞에 Stream을 키워드로 작성 후, async* 해주면 된다. 즉

위 처럼 루프를 돌면서 yield해주면 값이 return 처럼 도출된 후 함수가 끝나는게 아니라 계속해서 함수를 수행하는데, async*는 async의 기능도 가지고 있기에 await으로 딜레이를 주고 실행해보면, 

 

이렇게 asynchronous 하게 구동하면서 함수에 들어간 값을 Stream을 이용해 내보낼 수 있다.

 

만약 하나의 함수가 다 출력된 후 하나가 나왔으면 한다면,

import 'dart:async';

void main() {
  playAllStream().listen((val){
    print(val);
  });
}

Stream<int> playAllStream() async* {
  yield* calculate(1); // yield*은 stream의 모든 값을 가져올 때 까지 기다림 - future의 delay와 유사
  yield* calculate(1000);
}

Stream<int> calculate(int number) async* {
  
  // 루프를 돌면서 number값과 곱한 값을 루프 할때마다 돌려주도록 
  // return i * number하면 함수자체의 종료 때문에 한계 : Stream을 통해 극복가능
  for(int i =0; i < 5; i++){
    yield i * number;
    
    await Future.delayed(Duration(seconds: 1));
  }
}

위와 같이 Stream 함수로 playAllStream()을 만든 후, yield*를 작성해주는데 yield*는 모든 값을 수행할 때 까지 기다린다.

이를 수행하면 0, 1, 2, 3, 4, 0, 1000, 2000, 3000, 4000 순이 1초 딜레이 간격으로 나오는 것을 알 수 있다.