동기(Synchronous)와 비동기(Asynchronous)
동기/비동기는 주로 어플리케이션에서 자주 다뤄지는 개념이며, 다음 작업이 요청되는 시간과 관련되어 있다.
동기(Synchronous)
- 현재 작업의 응답이 끝남과 동시에 다음 작업이 요청된다.
- 함수를 호출하는 곳에서 호출되는 함수가 결과를 반환할 때까지 기다린다.
- 작업 완료 여부를 계속해서 확인한다.
비동기(Asynchronous)
- 현재 작업의 응답이 끝나지 않은 상태에서 다음 작업이 요청된다.
- 함수를 호출하는 곳에서 결과를 기다리지 않고, 다른 함수(callback)에서 결과를 처리한다.
- 작업 완료 여부를 확인하지 않는다.
동기 예시
function run(a, b) {
return a + b
}
const result = run(1, 2);
console.log("시작");
console.log("결과:", result);
console.log("끝");
/**출력 결과
* 시작
* 결과: 3
* 끝
*/
비동기 예시
function run(a, b) {
return a + b
}
let result;
setTimeout(() => {
result = run(1, 2);
}, 1000);
console.log("시작");
console.log("결과:", result);
console.log("끝");
/**출력 결과
* 시작
* 결과: undefined
* 끝
*/
블로킹(Blocking)과 논블로킹(Non-Blocking)
블로킹/논블로킹은 주로 멀티 스레딩, I/O 등에서 사용되는 개념이며, 함수의 리턴 시점과 제어권에 따라 차이가 난다.
블로킹(Blocking)
- 제어권이 호출된 함수에게 넘어가서 호출된 함수 내에서 작업이 모두 끝난 후 호출한 함수에게 다시 제어권이 넘어온다.
- 작업이 완료된 후 새로운 작업을 수행할 수 있다.
논블로킹(Non-Blocking)
- 제어권이 계속 호출한 함수에 있기 때문에 작업의 완료여부와 관계없이 새로운 작업을 수행할 수 있다.
블로킹 예시
function run() {
// 오래 걸리는 작업
console.log("작업 끝");
}
console.log("시작");
run();
console.log("다음 작업");
/** 출력 결과
* 시작
* 작업 끝
* 다음 작업
*/
논블로킹 예시
function run() {
// 오래 걸리는 작업
console.log("작업 끝");
}
console.log("시작");
setTimeout(run, 0);
console.log("다음 작업");
/** 출력 결과
* 시작
* 다음 작업
* 작업 끝
*/
이렇게만 보면 동기-블로킹, 비동기-논블로킹을 완전히 동일한 개념으로 생각할 수 있다. 물론 유사하게 동작하지만, 주요 관심사에 따라 차이가 난다. 앞서 언급한 호출한 함수 - 호출된 함수 이 개념을 상위 프로세스 / 하위 프로세스로 생각하면서 다음의 예시를 보자. 블로그의 예시를 좀 더 쉽게 표현하고자 노력했다(블로그에 설명이 정말 잘 되어 있으니 꼭 한 번 보면 좋을 것 같다).
상위 프로세스인 선생님(teacher)이 있고, 하위 프로세스인 학생들(student)이 있다고 가정하자. 각각의 선생님은 학생들에게 자습을 시키는데, 선생님들마다 각자의 자습 스타일이 있다. 이를 코드로 살펴보자.
동기 + 블로킹
동기+블로킹 선생님은 나이가 지긋한 FM 선생님이다. 입실 후 학생들의 자습이 모두 끝날때까지 아무것도 하지 않으면서 기다리고, 학생들의 자습이 끝나면 퇴실한다.
function student() {
for(i=1;i<11;i++) {
console.log(`학생: ${i}번 문제 푸는중...`);
}
}
function teacher() {
console.log("선생님: 입실");
student();
console.log("선생님: 퇴실");
}
teacher();
// 출력 결과
선생님: 입실
학생들: 1번 문제 푸는중...
학생들: 2번 문제 푸는중...
학생들: 3번 문제 푸는중...
학생들: 4번 문제 푸는중...
학생들: 5번 문제 푸는중...
학생들: 6번 문제 푸는중...
학생들: 7번 문제 푸는중...
학생들: 8번 문제 푸는중...
학생들: 9번 문제 푸는중...
학생들: 10번 문제 푸는중...
선생님: 퇴실
- 가장 많이 쓰이는 조합 중 하나이다.
- 모든 실행과 흐름이 순차적이기 때문에 개발자가 프로그램을 제어하기가 쉽다.
- 상위 프로세스(선생님)는 하위 프로세스(학생들)의 작업 완료 여부를 신경쓴다.
- 블로킹 방식이므로 하위 프로세스(학생들)의 작업이 완료되지 않으면 상위 프로세스(선생님)는 다른 작업을 할 수 없다.
동기 + 논블로킹
동기+논블로킹 선생님은 딴짓을 열심히 하는 선생님이다. 입실 후 학생들의 자습이 모두 끝날때까지 기다리면서 딴짓을 하면서 기다리고, 학생들의 자습이 끝나면 퇴실한다.
function* students() {
for(i=1;i<11;i++) {
console.log(`학생들: ${i}번 문제 푸는중...`)
yield;
}
return
}
function teacher() {
console.log("선생님: 입실");
const generator = students();
let done;
while (!done) {
done = generator.next().done;
if (!done) {
console.log("선생님: 딴짓")
}
};
console.log("선생님: 퇴실");
}
teacher();
// 출력 결과
선생님: 입실
학생들: 1번 문제 푸는중...
선생님: 딴짓
학생들: 2번 문제 푸는중...
선생님: 딴짓
학생들: 3번 문제 푸는중...
선생님: 딴짓
학생들: 4번 문제 푸는중...
선생님: 딴짓
학생들: 5번 문제 푸는중...
선생님: 딴짓
학생들: 6번 문제 푸는중...
선생님: 딴짓
학생들: 7번 문제 푸는중...
선생님: 딴짓
학생들: 8번 문제 푸는중...
선생님: 딴짓
학생들: 9번 문제 푸는중...
선생님: 딴짓
학생들: 10번 문제 푸는중...
선생님: 딴짓
선생님: 퇴실
- 상위 프로세스(선생님)는 하위 프로세스(학생들)의 작업 완료 여부를 신경쓴다.
- 논블로킹 방식이므로 하위 프로세스(학생들)의 작업 완료 여부와 상관 없이 상위 프로세스(선생님)는 다른 작업을 할 수 있다.
비동기 + 논블로킹
비동기+논블로킹 선생님은 아주 바빠서 자신의 시간이 소중하다. 입실 하자마자 자습을 지시한 후 바로 퇴실한다. 학생들의 자습이 끝나면 반장을 통해 보고받는다.
function students(callback) {
let i = 1;
const interval = setInterval(() => {
if (i > 10) {
callback();
clearInterval(interval);
} else {
console.log(`학생들: ${i}번 문제 푸는중...`);
i++;
}
}, 10);
}
function teacher() {
console.log("선생님: 입실");
students(() => console.log("반장: 선생님께 자습이 끝났음을 보고"));
console.log("선생님: 퇴실");
}
teacher();
// 출력 결과
선생님: 입실
선생님: 퇴실
학생들: 1번 문제 푸는중...
학생들: 2번 문제 푸는중...
학생들: 3번 문제 푸는중...
학생들: 4번 문제 푸는중...
학생들: 5번 문제 푸는중...
학생들: 6번 문제 푸는중...
학생들: 7번 문제 푸는중...
학생들: 8번 문제 푸는중...
학생들: 9번 문제 푸는중...
학생들: 10번 문제 푸는중...
반장: 선생님께 자습이 끝났음을 보고
- 가장 많이 쓰이는 조합 중 하나이다.
- 성능과 자원의 효율면에서 가장 우수하다.
- 상위 프로세스(선생님)는 하위 프로세스(학생들)의 작업 완료 여부를 신경쓰지 않는다.
- 논블로킹 방식이므로 하위 프로세스(학생들)의 작업 완료 여부와 상관 없이 상위 프로세스(선생님)는 다른 작업을 할 수 있다.
비동기 + 블로킹
- 상위 프로세스는 하위 프로세스의 작업 완료 여부를 신경쓰지 않는다.
- 블로킹 방식이므로 하위 프로세스의 작업이 완료되지 않으면 상위 프로세스는 다른 작업을 할 수 없다.
이 방식은 다른 블로그에서 설명이 많이 부족한 편이었는데, 자료마다 의견이 제각각이었다.
참고한 글에서도 적절한 코드가 없었기에 생략했다.
1. 비동기+논블로킹 방식을 사용하는 과정에서 의도치 않게 사용됨
- Node.js + MySQL의 조합이 대표적인데, Node.js에서 비동기(async)적으로 DB 작업을 수행하더라도 블로킹 방식으로 작동하는 MySQL 드라이버를 거치게 된다.
- 별 다른 장점이 없어서 일부러 사용할 필요는 없지만, 비동기+논블로킹 방식을 사용하는 과정에서 하나라도 블로킹 방식을 사용하는 함수가 있다면 의도치 않게 비동기+블로킹 방식으로 작동할 수 있다.
2. 직관적인 코드의 흐름을 유지하면서 작업을 병렬적으로 처리하기 위함
- 이 개념은 효율적이지 않은 것처럼 보이지만, 다음과 같은 이유에서 등장했다.
1. 동기 & 블록킹 I/O의 경우 직관적이나, 여러 개의 I/O를 동시에 처리할 수 없다.
2. 논블록킹 I/O는 프로세스들의 작업을 컨트롤하는 것이 까다롭다. (대부분 이런 저레벨 프로그램은 C로 짠다. JS나 Python 같은 걸 생각하면 안된다.)
3. 그렇다고 동기 & 블록킹 I/O와 멀티 프로세싱이나 쓰레딩을 결합해서 쓰자니 자원 문제도 있고 프로세스/쓰레드 간 통신이나 동기화가 빡셈
- 따라서 직관적인 코드의 흐름을 유지하면서 작업을 병렬적으로 처리하기 위해 Linux/UnixS OS의 I/O 다중화 모델 등에서 사용된다.
참고