이번에는 Dart의 동기 - 비동기에 대해서 알아보겠습니다. 알아보기 전에 Dart는 하나의 스레드 ( SingleThread )로 동작합니다.
즉 한 번에 하나의 프로세스 처리만 할 수 있습니다. 예를 들자면 A라는 프로세스와 B라는 프로세스를 처리해야 한다고 하면 A의 프로세스 작업을 끝내야지만 B의 프로세스를 처리할 수 있습니다. 한 번에 하나만 처리를 할 수 있으니깐요.
1. 동기적 - Synchronous
동기적(Synchronous) 실행은 간단하게 말하면 순차적인 실행을 의미합니다. 어떤 작업을 실행하면 그 작업이 완료될 때까지 다음 작업으로 넘어가지 않고 대기하는 방식입니다. 예를 들어, 동기적인 작업에서는 특정 함수를 호출하면 그 함수가 모든 작업을 완료할 때까지 다음 코드로 진행되지 않습니다. 작업이 완료되어야만 제어가 호출된 곳으로 돌아올 수 있습니다.
void main() async {
a();
b();
}
void a() {
print("a");
print("a");
print("a");
print("a");
}
void b() {
print("b");
print("b");
print("b");
print("b");
}
//출력
a
a
a
a
b
b
b
b
간단하게 코드로 보면 a() 함수가 호출되면 a함수의 모든 작업을 끝내고 나서 b() 함수가 호출되고 해당 작업을 합니다.
2. 비동기 - Asynchronous
비동기는 간단하게 설명하면 동기의 반대입니다. 즉 순차적으로 처리를 하지 않는다는 것입니다. 그렇다면 동기적으로 작업을 다 처리하면 될 거 같은데 왜 비동기를 사용하는 것일까요? 이것은 앞서 설명한 dart는 single thread로 동작한다는 것을 생각하면 쉽게 이해할 수 있습니다.
void main() async {
a();
b();
}
void a() {
(5초가 걸리는 서버 호출 작업);
print("a");
}
void b() {
print("b");
print("b");
print("b");
print("b");
}
해당 코드에서 a() 함수를 호출해서 작업을 하려 하는데 서버에서 데이터를 받아오는 등 무거운 작업을 해서 5초가 걸린다고 가정해 봅시다. 해당 코드가 비동기가 아닌 동기로 순차작업을 한다고 하면 5초간의 무거운 작업이 끝나고 나서야 "a"가 출력되고 그다음에서야 "b"가 출력될 것입니다.
이것이 문제 될 점은 서버에서 데이터를 받아오는 5초간 Thread는 처리할 작업이 없습니다. 5초간 서버에서 데이터가 오는 것을 기다리고만 있는 것입니다. 이 아까운 5초에 "a"를 출력하거나 b함수 작업을 수행해 "b"를 출력할 수도 있을 텐데 말이죠.
우리는 SingleThread인 dart의 thread를 이렇게 놀게 할 수 없습니다. thread를 쉬지 않고 일하게 시키려고 비동기 처리를 하는 이유입니다.
void main() async {
a();
b();
}
void a() {
Future.delayed(Duration(seconds: 5))
.then((e) => print("데이터를 가져왔어요."));
print("a");
}
void b() {
print("b");
print("b");
print("b");
print("b");
}
//출력
a
b
b
b
b
데이터를 가져왔어요.
위 코드에서 Future.delayed 함수가 대표적으로 비동기 함수로 볼 수 있습니다. 5초를 기다린다음 print를 찍는 함수입니다. 해당 함수는 비동기 함수이기 때문에 순차적으로 처리하지 않습니다. 그래서 기다리는 시간 동안 thread는 다음 작업을 처리하기 때문에 출력 결과가 위처럼 나오게 되는 것입니다.
3. 이벤트큐 - EventQueue
void main() async {
a();
b();
}
void a() {
Future.delayed(Duration(milliseconds: 1))
.then((e) => print("A 데이터를 가져왔어요."));
print("a");
}
void b() async {
double num = 0;
for(int i = 0; i < 10000000000; i++) {
num += i;
}
print(num);
}
이벤트큐를 알아보기 위해 해당 코드를 살펴보고 예상되는 출력값을 생각해 봅시다.
1)
//출력
(num 계산값)
A 데이터를 가져왔어요.
a
2)
//출력
a
(num 계산값)
A 데이터를 가져왔어요.
3)
//출력
a
A 데이터를 가져왔어요.
(num 계산값)
1번을 고르셨다면 다시 위로 올라가서 한 번 더 이해하는 게 좋을 거 같아요.
3번을 고르셨다면 그럴 수 있다고 생각합니다. 왜냐하면 num을 계산하기 전에 1 milliseconds가 끝났을 거 기 때문에 해당작업이 끝나고 then으로 "A 데이터를 가져왔어요."를 출력할 거 같은데?라고 생각을 할 수 있습니다.
하지만 정답은 2번입니다. 이것을 설명하기 위해서 이벤트큐를 간단하게나마 알아야 합니다.
해당 코드의 동작으로 1번째로 a함수가 호출되고 비동기 함수를 만나게 됩니다. 해당 비동기 작업이 끝나고 실행되는 작업은 이벤트큐로 들어가게 됩니다. print("A 데이터를 가져왔어요.") 이 작업이 이벤트 큐로 들어갑니다.
2번째로 a 함수의 나머지 작업인 print("a"); 가 실행되고 a 함수의 작업은 남아있지 않기 때문에 b함수가 호출되고 무거운 연산 작업을 수행하게 됩니다. 해당 연산 작업을 끝내고 결과를 출력하고 나면 더 이상 수행해야 할 작업이 남아 있지 않게 됩니다. 이때 이벤트큐에 들어있는 작업을 처리하게 됩니다.
이벤트 큐에서 꺼낸 작업의 비동기 처리가 끝났는지 확인이 되면 해당 작업을 수행하고 나서 더 이상 이벤트큐에 남아있는 작업이 없으면 끝나게 되는 것입니다. 그래서 2번과 같은 출력값이 나오는 겁니다. 여기서는 간단하게 이해되게 설명한 거지 더 복잡하고 심오한 작업을 할 것입니다.
이벤트큐는 결국 큐니깐 FIFO 방식으로 먼저 들어온 작업을 먼저 꺼내서 작업한다는 것을 기억해 주세요.
4. 추가로
void main() async {
a();
b();
}
void a() {
Future.delayed(Duration(milliseconds: 500))
.then((e) => print("A 데이터를 가져왔어요."));
print("a");
}
void b() async {
double num = 0;
for(int i = 0; i < 1000000; i++) {
num += i;
}
print(num);
}
해당 코드의 동작에서 num을 구하는 중에 a의 작업에서 비동기작업이 끝날 것이니깐 해당작업이 끝나면 이것을 먼저 처리하고 그 뒤에 다시 num을 계산할래! 할 수 있습니다.
void main() async {
a();
b();
}
void a() {
Future.delayed(Duration(milliseconds: 500))
.then((e) => print("A 데이터를 가져왔어요."));
print("a");
}
void b() async {
double num = 0;
for(int i = 0; i < 1000000; i++) {
await Future.delayed(Duration(seconds: 0));
num += i;
}
print(num);
}
이렇게 반복문 사이에 await을 걸고 비동기 함수인 Future.delayed를 실행하면 남은 작업을 이벤트큐에 넣고 그 뒤로 처리할 작업이 없으니 이벤트큐에서 먼저 들어온 작업을 꺼내와서 끝났는지 확인하고 끝나지 않았다면 다시 이벤트큐에 집어넣고 그다음 이벤트큐에 들어있는 작업을 또 꺼내와서 확인하고를 반복하다가 결국엔 원하는 동작인 num을 다 구하기 전에 "A 데이터를 가져왔어요."를 출력하고 num의 계산결과를 출력하는 동작을 하게 될 것입니다.
그런데 방금 설명한 것이 이해가 되셨나요? 해당코드로 원하는 동작을 한다고는 하지만 a의 끝나는 시간을 직접 우리가 milliseconds: 500으로 지정했기에 먼저 끝난다는 것을 알아서 그렇지 사실 저부분은 서버에서 데이터를 가져오는 등 끝나는 시간을 예측할 수 없는 작업일 것입니다. 그럼 저런 이상한 동작을 하더라도 원하는 동작으로 실행 안될 수 있습니다. 그리고 저런 동작은 성능 저하를 일으킬 수 있습니다.
간단하게 설명해서 이벤트큐로 계속 왔다 갔다 하는 것도 있고 Duration(seconds: 0)으로 0초를 지연시켰다고 해도 dart에서 최소 지연시간은 2.7 밀리초라고 합니다. 이만큼 지연되는 되는 만큼 당연히 좋지 않겠죠.
이럴 때는 dart에서 제공하는 병렬 작업 방식인 Isolate를 사용해서 작업해야 합니다.
'Flutter > Dart' 카테고리의 다른 글
[Dart] DateUtils Class 정리 (날짜 작업을 위한) (0) | 2024.01.09 |
---|---|
[Dart] fold() - 리스트 순회 계산 (0) | 2023.07.21 |
[Dart] Ceil - 올림처리 (0) | 2023.07.18 |