Blocking 과 Non-Blocking
사람들이 굉장히 많이 헷갈려하는 Blocking 과 Non-Blocking에 대해 알아보자
중요한것은 Blocking 과 Non-Blocking은 동기와 비동기와는 관점을 다르게 봐야한다.
Blocking과 Non-Blocking은 제어권이 어디에 있느냐에 대한 관점이다
작업을 시킨 주체에게 제어권이 있는지 혹은 작업을 처리하는 주체에게 제어권이 있는지에 대한 것이다.
- Blocking : 직접 제어를 할 수 없는 대상의 작업이 끝날 때까지 기다려야 하는 경우
- Non-Blocking : 직접 제어할 수 없는 대상의 작업이 완료되기 전에 제어권을 넘겨주는 경우
Blocking
직접 제어를 할 수 없는 대상의 작업이 끝날 때까지 제어권을 넘겨주지 않는 것이다.
예를 들어 호출하는 함수가 I/O 작업을 요청한다면 I/O 처리가 완료될 때까지 아무 일도 하지 못하고 기다리는 것을 말한다.
Blocking은 개발 부서 전체 작업의 제어권을 가진 팀장이 엔지니어 A에게 넘겨주게 된다. 이때 엔지니어 A가 작업을 수행하는 동안 개발 부서 전체는 이를 기다리게 되는 것이다.
엔지니어 A가 작업을 마치고 팀장에게 알리면서 동시에 제어권도 돌려주게 된다.
※ 제어권이 없는 상태에서는 Blocking이 되며 다른 일을 할 수 없는 상태가 된다.
Non-Blocking
직접 제어할 수 없는 대상의 작업처리 여부와 상관이 없다!
예를 들어 호출하는 함수가 I/O 작업을 요청하고 I/O 작업에 대한 처리 완료 여부와는 상관없이 바로 자신의 작업을 수행할 수 있는 것을 말한다.
Non-Blocking은 제어권을 바로 팀장에게 돌려준다. 때문에 개발 부서는 엔지니어 A의 작업을 기다리지 않는다.
즉, 다른일을 할 수 있는 상태가 되는 것이다.
동기(Synchronous)와 비동기(Asynchronous)
동기
메소드를 실행시킴과 동시에 반환 값이 기대되는 경우를 표현하는 방법이다
이때 동시라는 말은 실행되었을 때 값이 반환되기 전까지는 Blocking 되어 있다는 것을 의미하게 된다.
비동기
동기와는 반대되는 의미로 Blocking이 되지 않고 이벤트 큐에 넣거나 백그라운드 스레드에게 해당 task를 위임하고 바로 다음 코드를 실행하기 때문에 기대되는 값이 바로 반환되지 않는다.
Synchronous(동기)
두 개 이상의 주체가 작업 시간을 똑같이 맞추어 행동하는 것이다.
예를 들면 호출한 함수가 호출된 함수의 작업이 끝나 결과를 반환하기를 기다리거나, 지속적으로 호출된 함수에게 확인을 요청하는 경우가 있다.
위의 사진을 보면 엔지니어 A가 끝나는 시간과 엔지니어 B가 시작하는 시간을 맞추어 작업하는 것을 Synchronous(동기)이다. 예를 들면 자바의 synchronized 와 BlockingQueue가 있다.
또 다른 Synchronous를 예로 들어보자
엔지니어 A와 엔지니어 B가 시작시간 또는 종료 시간이 일치하게 되면 Synchronous(동기)이다.
예를 들면 이런 것들이 있을 것이다.
- A와 B 스레드가 동시에 작업을 시작하는 경우( 자바의 CyclicBarrier)
- 메소드 리턴 시점(A)과 결과를 전달받는 시점(B)이 일치하는 경우
정리(Blocking 과 Synchronous의 차이)
Blocking은 작업이 끝나기를 기다리다가 끝나면 다음 작업을 수행하는 것이다.
Synchronous는 두 가지 이상의 대상이 서로 시간을 맞춰 행동하는 것이다.
좀 더 쉽게 말해보면 Blocking은 작업 수행하는 턴을 의미한다. 티키타카를 생각하면 편하지 않을까?
그리고 Synchronous는 두개의 스레드의 작업하는 시간을 중점으로 생각하면 좋을것 같다 그것이 시작이던 종료이던 말이다.
Asynchronous(비동기)
비동기는 작업을 수행하는 주체의 시작 시간과 끝나는 시간에 관계없이 각자 별도의 시작 시간, 끝나는 시간을 가지고 있을 경우이다.
즉, 서로 무슨 일을 하던간에 별 관심없이 행동한다고 볼 수 있다.
정리(Non-Blocking 과 Asynchronous의 차이)
Non-Blocking은 다른 작업이 끝날때까지 기다리지 않는 것이고 Asynchronous는 두가지 이상의 대상이 서로 시간을 맞춰 행동하지 않는다는 것이다.
각각의 조합
1. Synchronous + Blocking
우리가 가장 흔하게 접하는 방식인 동기와 블록킹 방식이다. 동기 방식을 기반으로 작업하기 때문에 작업의 흐름도 순차적으로 진행되는 것이 보장되고, 블록킹 방식이기 때문에 어떠한 작업이 진행 중일때는 다른 작업을 동시에 진행 할 수 없다.
function employee() {
for (let i = 1; i < 101; i++) {
console.log('직원: 인형 눈알 붙히기 ${i}번 수행');
}
}
function boss() {
console.log('사장: 출근');
employee();
console.log('사장: 퇴근);
}
boss();
출력 결과
사장: 출근
직원: 인형 눈알 붙히기 1번 수행
직원: 인형 눈알 붙히기 2번 수행
...
직원: 인형 눈알 붙히기 100번 수행
사장: 퇴근
이렇게 코드를 보면 우리는 자연스럽게 이 작업들이 순서를 가지고 진행 될 것이라는 것을 알수가 있다. 내부적으로는 하나의 콜 스택에 작업을 넣고 Last In First Out으로 진행되기 때문이라는 것을 알고 있지만, 여기서는 그런 내부 로직보다는 그냥 작업이 순서대로 진행된다는 것이라는 것에 집중해야한다.
상위 프로세스인 boss 함수는 출근 작업을 수행한 뒤에 하위 프로세스인 employee 함수에게 인형 눈알 붙히기 작업을 요청하고 있고, 이 인형 눈알 붙히기 작업이 완료되고 나서야 boss 함수는 퇴근 작업을 수행한다.
쉽게 말해서 작업을 시킨놈인 상위 프로세스는 작업을 하는 놈인 하위 프로세스가 종료될 때까지 절대 퇴근할 수 없다는 것이다. 이 예제와 같이 동기 방식과 블록킹 방식을 함께 사용하는 매커니즘은 일반적으로 사람들이 동기 방식이라고 하면 가장 먼저 떠올리는 방식이고 직관적으로 이해하기도 쉬운 편에 속한다.
2. Synchronous + Non-Blocking
동기적인 작업의 흐름을 유지하면서 employee 함수가 인형의 눈알을 붙히는 동안 boss 함수가 다른 일을 할 수 있는 방법이 있다.
동기라는 것은 작업들이 순차적인 흐름을 가지고 있다는 것을 의미하기 떄문에 이 전제만 지켜진다면 나머지는 어떻게 되든 간에 동기 방식이라는 것은 변하지 않기 때문이다. 그래서 동기는 블로킹이 될 수 없다
작업의 순서를 지키면서 상위 프로세스가 다른 작업을 하도록 만드는 JavaScript의 제너레이터를 사용한 예시를 보자
function* employee() {
for(let i = 1; i < 101; i++) {
console.log(`직원: 인형 눈알 붙히기 ${i}번 수행`);
yield;
}
return;
}
function boss() {
console.log('사장: 출근');
const generator = employee();
let result = {};
while(!result.done) {
result = generator.next();
console.log(`사장: 유튜브 시청...`);
}
console.log('사장: 퇴근');
}
boss();
출력 결과
사장: 출근
직원: 인형 깔알 붙히기 1번 수행
사장: 유튜브 시청...
직원: 인형 눈알 붙히기 2번 수행
사장: 유튜브 시청...
...
직원: 인형 눈알 붙히기 100번 수행
사장: 유튜브 시청...
사장: 퇴근
이 예제를 보면 상위 프로세스인 boss 함수는 출근한 후 하위 프로세스인 employee를 호출하여 인형 눈알 붙히기작업을 시키고 주기적으로 이 작업이 끝났는지를 검사한다.
그리고 아직 작업이 끝나지 않았다면 자신 또한 열심히 유튜브 시청을 수행하는 것을 볼 수 있다. 이 코드는 분명히 동기적인 흐름을 가지고 있다 하짐나 boss 함수 또한 중간중산 자신의 작업을 수행하고 있기때문에 블록킹이 아니라 논블록킹을 사용하고 있는 것이다.
이 예제에서도 동기 & 블록킹 방식과 마찬가지로 boss 함수는 employee 함수의 작업이 끝나기 전까지는 절대 퇴근을 할 수 없다. 작업의 순서가 지켜지고 있는 것이다. 즉, 동기 방식이라는 것은 작업의 순차적인 흐름만 지켜진다면 블록킹이든 논블로킹이든 아무 상관이 없다고 할 수 있다.
3. Asynchronous + Non-Blocking
비동기 방식과 논블로킹 방식을 조합한 방법은 우리에게 굉장히 익숙하다. 비동기 방식이기에 상위 프로세스는 하위 프로세스의 작업 완료 여부를 따로 신경쓰지 않는다. 이후 하위 프로세스의 작업이 종료되면 스스로 상위 프로세스에게 보고를 하든 아니면 다른 프로세스에게 일을 맡기든 할 것이다.
논 블로킹 방식이기 때문에 상위 프로세스는 하위 프로세슬에게 일을 맡기고 자신의 작업을 계속 수행할 수도 있다.
function employee (maxDollCount = 1, callback) {
let dollCount = 0;
const interval = set Interval(() => {
if (dollCount > maxDollCount) {
callback();
clearInterval(interval);
}
dollCount++;
console.log(`직원: 인형 눈알 붙히기 ${dollCount}번 수행`);
}, 10);
}
function boss() {
console.log('사장: 출근');
employee(100, () => console.log('직원: 눈알 결산 보고'));
console.log('사장: 퇴근');
}
boss();
출력 결과
사장: 출근
사장: 퇴근
직원: 인형 눈알 붙히기 1번 수행
직원: 인형 눈알 붙히기 2번 수행
...
직원: 인형 눈알 붙히기 100번 수행
직원: 눈알 결산 보고
이 예제를 보면 boss 함수는 employee 함수에게 인형 눈알 100개를 붙히라고 지시한 후 자신은 바로 퇴근해버렸다. 상위 프로세스인 boss 함수는 employee 함수의 작업이 언제 끝나는지는 관심이 없다. 작업의 완료 신호는 콜백으로 넘겨진 눈알 결산 보고 작업이 대신 받아서 처리하게 된다.
비동기 & 논블로킹 방식은 여러 개의 작업을 동시에 처리할 수 있는 부분에서 효율적이다. 하지만 너무 복잡하게 얽힌 비동기 처리 때문에 개발자가 어플리케이션의 흐름을 읽기 어려워지는 등의 문제가 발생한다. JavaScript에서 Promise나 async/await같은 문법을 사용하는 이유또한 이런 비동기 처리의 흐름을 좀 더 명확하게 인지하고자 하는 노력이 될 것이다.
4. Asynchronous + Blocking
이 방식은 일반적인 어플리케이션 레이어에서는 자주 사용되지 않는다. Linux나 Unix 운영체제의 I/O 다중화 모델 정도의 저레벨에서 사용되는데 이 개념은 얼핏 들으면 비효율적이다. 비동기 방식의 장점은 하위 프로세스의 작업이 끝나는 것을 기다리지 않고 여러 개의 작업을 동시에 처리 할 수 있다는 것이다. 하지만 프로세스가 블로킹 되어 버리면 유휴상태로 빠져 아무것도 처리할 수 없기 때문이다.
그렇다면 왜 이러한 개념이 나타난 것일까?
1. 동기 & 블로킹 I/O의 경우 직관적이나, 여러 개의 I/O를 동시에 처리 할 수 없다.
2. 논블록킹 I/O는 프로세스들의 작업을 컨트롤하는 것이 까다롭다.
3. 그렇다고 동기 & 블록킹 I/O와 멀티 프로세싱이나 쓰레딩을 결합해서 쓰자니 자원 문제도 있고 프로세스/쓰레드 간 통신이나 동기화가 빡세다.
" 그럼 그냥 프로세스를 블록킹 하고 비동기로 여러 개의 I/O를 다중화해서 받아버리자"
이것이 비동기 & 블록킹 개념이 나오게 된 이유이다. 이것은 직관적인 코드의 흐름을 유지하면서 작업을 동시에 처리하겠다 라는 뜻이다.
위의 그림을 보면 중간에 초록색 바로 select()가 존재한다. 이 것이 바로 프로세스를 블록킹함과 동시에 여러 개의 I/O를 받아서 처리하는 역할을 하게 된다. 이 함수는 C언어의 API로 제공되고 있으며, 그냥 include<sys/select.h>와 같이 헤더를 가져와서 사용하면 된다.
int select (int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
이때 nfds는 감시할 파일의 갯수를 fd_set 구조체들은 각각 읽을 데이터, 쓰여진 데이터, 예외 처리가 발생한 것을 감시할 파일 목록이다. 이때 fd...은 File Descriptor의 약자이며, 파일을 감시하고 있다가 해당 I/O가 발생하면 자신이 가지고 있는 비트 배열 구조체의 해당 값을 1로 변경한다.
그리고 timeval구조체인 timeout 인자는 감시할 시간을 의미한다. 즉, 이timeout인자에 넘겨준 시간 동안 상위 프로세스를 블록킹하면서 자신이 넘겨받은 파일 목룍을 계속 감시하게 되는 것이다. 또한 감시하는 동안 읽기, 쓰기, 예외가 발생하면 select 함수가 종료될 때 자신이 감시하던 있는 파일들 중 해당 변경사항이 발생한다, 즉 처리해야할 파일의 갯수를 반환한다.
이정도가 비동기 &블록킹 방식의 대표적인예인 select의 함수가 작동하는 방식이다.
일정 시간동안 프로세스를 멈추고 자신이 감시하고 있는 파일들에서 I/O가 발생하는지를 감시한다 그 후에 일정 시간이 지나면 함수가 종료되고 그동안 감시했던 파일들의 I/O결과를 반환하고 프로세스의 블록킹이 풀리게 되는 것이다.
이 방식은 블록킹 방식으로 진행되기 떄문에 개발자에게도 직관적으로 다가오고 비동기 방식이기 떄문에 여러 개의 I/O를 동시에 감시하며 처리 할 수 있다. 하지만 성능이 그다지 좋지 않기때문에 높은 성능을 필요로 하는 어플리케이션에서 사용하는 것은 권장하지 않는다.
'CS > 💻 운영체제' 카테고리의 다른 글
💻 커널이란 (0) | 2024.05.29 |
---|---|
💻 프로세스와 스레드 (0) | 2023.10.10 |