쉽게 말해서 코드를 위에서부터 아래로 하나씩 순서대로 실행하는 것.
즉, 먼저 실행된 코드가 끝나야 다음 코드를 실행시킴.
10분 걸리는 함수 A가 있고 1초 걸리는 함수 B가 있다고 했을 때,
func A();
func B();
이렇게 코드를 실행하면 A가 끝날 때 까지 B는 실행하지 못하므로 프로그램 전체가 끝날 때 까지 10분 1초가 걸린다.
동기가 코드를 순차적으로 실행하는 것이라면,
비동기는 그 반대로 순차적으로 실행하지 않는 것이다.
앞서 들었던 예시에서 B 함수 밑에 100개의 함수가 더 있다고 가정해보자.
만약에 이 코드를 순차적으로 실행하게 된다면 앞에 함수가 끝날 때 까지 기다려줘야 하기 때문에 시간이 엄청 많이 걸릴 것이다(당장에 A 함수가 끝나는데만 10분이 걸린다. 이게 쌓이다 보면...)
이 문제를 해결하려면 오래 걸리는 함수는 일단 제치고 빨리 걸리는 함수부터 싹 다 실행해 놓고 맨 마지막에 오래 걸리는 함수를 처리하는 것이 시간이 덜 걸릴 것이다.
마찬가지로 실제 앱에서도 시간이 오래 걸리는 계산이나, 서버로부터 이미지 등 데이터를 받아오는 작업을 실행 해 놓고, 실행 시간 동안 빨리 할 수 있는 다른 코드를 돌리는 것이 효율적일 것이다.
비동기 프로그래밍을 위해 dart 에서는 Future 클래스를 활용한다.
Future 클래스는 말 그대로 미래에 받아올 값을 뜻한다.
List 나 Set 처럼 제네릭으로 어떤 미래의 값을 받아올지를 정할 수 있다.
void main(){
Future<String> name; // 미래에 받을 String값
Future<int> number; // 미래에 받을 int값
Future<bool> isOpened; // 미래에 받을 boolean값
}
특정 기간동안 아무것도 하지 않고 기다리는 Future.delayed(); 를 사용한 예제를 보자
void main() {
addNumbers(1, 1);
}
void addNumbers(int number1, int number2){
print('$number1 + $number2 계산 시작!');
// ➊ Future.delayed()를 사용하면 일정 시간 후에 콜백 함수를 실행할 수 있음
Future.delayed(Duration(seconds: 3), (){
print('$number1 + $number2 = ${number1 + number2}');
});
print('$number1 + $number2 코드 실행 끝');
}
// 실행 결과
1 + 1 계산 시작! #2
1 + 1 코드 실행 끝 #3
1 + 1 = 2 #4
Future.delayed() 메소드는 첫번째 파라미터에 대기할 기간을 Duration(seconds: 3) 이렇게 입력하고, 두번째 파라미터에 대기 후 실행할 콜백 함수를 입력하면 된다.
즉 위의 코드는 3초를 기다리고
해당 콜백함수를 실행한다.
(){
print('$number1 + $number2 = ${number1 + number2}');
}
언뜻 보면
print('$number1 + $number2 계산 시작!');
print('$number1 + $number2 = ${number1 + number2}');
print('$number1 + $number2 코드 실행 끝');
이 순서대로 실행되어야 할 것 같지만
Future.delayed() 는 비동기 연산이기 때문에 3초 대기하는 동안 먼저 밑에 코드를 실행한다.
따라서 결과는 아래처럼 되는 것이다.
print('$number1 + $number2 계산 시작!');
print('$number1 + $number2 코드 실행 끝');
print('$number1 + $number2 = ${number1 + number2}');
이렇게 비동기 프로그래밍을 하면 3초의 시간을 낭비하지 않고 미리 다른 코드를 실행시켜서 효율적으로 프로그램을 돌릴 수 있다.
Future 사용법을 배워서 코딩을 했는데, 나중에 돌아와서 보니까 어떤 순서대로 실행되는지 잘 몰라서 난감할 수 있다.
이럴 때, async 와 await 키워드를 사용해서 비동기 프로그램의 코드 가독성을 높일 수 있다.
void main() {
addNumbers(1, 1);
}
// async 키워드는 함수 매개변수 정의와 바디 사이에 입력합니다.
void addNumbers(int number1, int number2) async {
print('$number1 + $number2 계산 시작!');
// await는 대기하고 싶은 비동기 함수 앞에 입력합니다.
await Future.delayed(Duration(seconds: 3), (){
print('$number1 + $number2 = ${number1 + number2}');
});
print('$number1 + $number2 코드 실행 끝');
}
// 실행 결과
1 + 1 계산 시작!
1 + 1 = 2
1 + 1 코드 실행 끝
비동기로 실행할 함수를 async 로 지정해 주고 나서 비동기 함수를 실행할 때, await 키워드를 사용하면 된다.
이렇게 하면 동기 프로그래밍과 뭐가 다른가 싶지만 async, await 키워드를 사용하면 비동기 프로그래밍 특징을 그대로 유지하면서 코드가 작성된 순서대로 프로그램을 실행한다.
다음 예제를 보면 이해가 될 것이다.
void main() {
addNumbers(1, 1);
addNumbers(2, 2);
}
// async 키워드는 함수 매개변수 정의와 바디 사이에 입력합니다.
void addNumbers(int number1, int number2) async {
print('$number1 + $number2 계산 시작!');
// await는 대기하고 싶은 비동기 함수 앞에 입력합니다.
await Future.delayed(Duration(seconds: 3), (){
print('$number1 + $number2 = ${number1 + number2}');
});
print('$number1 + $number2 코드 실행 끝');
}
// 실행 결과
1 + 1 계산 시작!
2 + 2 계산 시작!
1 + 1 = 2
1 + 1 코드 실행 끝
2 + 2 = 4
2 + 2 코드 실행 끝
addNumbers() 함수는 두번 호출되었다. 출력 결과를 1,1 입력과 2,2 입력으로 따로 나눠서 보면 예상한 대로 잘 출력된 것을 알 수 있다.
그런데 addNumbers(1,1)이 끝나기 전에 addNumbers(2,2) 가 실행된 것을 볼 수 있다.
왜냐하면 addNumbers() 함수는 비동기 함수 이기 때문이다.
addNumbers(1,1)의 Future.delayed() 가 실행되서 3초를 기다릴 동안 함수를 나와서 바로 다음 실행할 코드인 addNumbers(2,2)를 실행한 것이다!
만약 addNumbers(1,1)과 addNumbers(2,2) 가 순차적으로 실행되길 원하면 다음과 같이 수정할 수 있다.
void main() async {
await addNumbers(1, 1);
await addNumbers(2, 2);
}
// async 키워드는 함수 매개변수 정의와 바디 사이에 입력합니다.
Future<void> addNumbers(int number1, int number2) async {
print('$number1 + $number2 계산 시작!');
// await는 대기하고 싶은 비동기 함수 앞에 입력합니다.
await Future.delayed(Duration(seconds: 3), (){
print('$number1 + $number2 = ${number1 + number2}');
});
print('$number1 + $number2 코드 실행 끝');
}
// 실행 결과
1 + 1 계산 시작!
1 + 1 = 2
1 + 1 코드 실행 끝
2 + 2 계산 시작!
2 + 2 = 4
2 + 2 코드 실행 끝
원하는 대로 함수가 순차적으로 실행된 것을 알 수 있다.
void main() async {
final result = await addNumbers(1, 1);
print('결괏값 $result'); // 일반 함수와 동일하게 반환값을 받을 수 있음
final result2 = await addNumbers(2, 2);
print('결괏값 $result2');
}
Future<int> addNumbers(int number1, int number2) async {
print('$number1 + $number2 계산 시작!');
await Future.delayed(Duration(seconds: 3), (){
print('$number1 + $number2 = ${number1 + number2}');
});
print('$number1 + $number2 코드 실행 끝');
return number1 + number2;
}
await 키워드를 적용해도 일반 함수처럼 변수에 반환값을 저장하고 활용할 수 있다.
Future는 반환값을 딱 한번 받아내는 비동기 프로그래밍에 사용한다.
반면에 지속적으로 반환값을 받을 때는 Stream을 사용한다.
Stream은 한번 listen 하면 Stream에 주입되는 모든 값들을 지속적으로 받아온다.
즉, Stream은 이벤트 기반의 작업이나, 여러 개의 값을 처리하는 데 적합하다.
import 'dart:async';
void main() {
final controller = StreamController(); // StreamController 선언
final stream = controller.stream; // Stream 가져오기
// Stream에 listen() 함수를 실행하면 값이 주입될 때마다 콜백 함수를 실행할 수 있습니다.
final streamListener1 = stream.listen((val) {
print(val);
});
// Stream에 값을 주입할 때는 sink.add() 함수를 실행하면 됩니다.
controller.sink.add(1);
controller.sink.add(2);
controller.sink.add(3);
controller.sink.add(4);
}
// 실행결과
1
2
3
4
스트림은 단 한번만 listen()을 실행할 수 있다.
하지만 때때로 하나의 스트림을 생성하고 여러번 listen() 함수를 실행하고 싶을 때가 있다.
이럴 때, 브로드캐스트 Stream을 사용하면 여러번 listen() 하도록 변환할 수 있다.
import 'dart:async';
void main() {
final controller = StreamController();
// 여러 번 리슨할 수 있는 Broadcaste Stream 객체 생성
final stream = controller.stream.asBroadcastStream();
// 첫 listen() 함수
final streamListener1 = stream.listen((val) {
print('listening 1');
print(val);
});
// 두 번째 listen() 함수
final streamListener2 = stream.listen((val) {
print('listening 2');
print(val);
});
// add()를 실행할 때마다 listen()하는 모든 콜백 함수에 값이 주입됩니다.
controller.sink.add(1);
controller.sink.add(2);
controller.sink.add(3);
}
// 실행결과
listening 1
1
listening 2
1
listening 1
2
listening 2
2
listening 1
3
listening 2
3
StreamController를 직접 사용하지 않고도 직접 스트림을 반환하는 함수를 작성할 수도 있다.
Future를 반환하는 함수는 async로 함수를 선언하고 return 키워드로 값을 반환하면 된다.
Stream을 반환하는 함수는 async* 로 함수를 선언하고 yield 키워드로 값을 반환해주면 된다.
import 'dart:async';
// Stream을 반환하는 함수는 async*로 선언합니다.
Stream<String> calculate(int number) async* {
for (int i = 0; i < 5; i++) {
// StreamController의 add()처럼 yield 키워드를 이용해서 값 반환
yield 'i = $i';
await Future.delayed(Duration(seconds: 1));
}
}
void playStream() {
// StreamController와 마찬가지로 listen() 함수로 콜백 함수 입력
calculate(1).listen((val) {
print(val);
});
}
void main() {
playStream();
}