저를 포함한 많은 분들이
blocking과 synchronous,
non-blocking과 asynchronous의 차이점을 명확하게 알지 못하는 것 같아,
이번 글에서는 위 개념들에 대해 공부하면서
이해한 것들을 공유해보려고 합니다.
(혹시나 잘못 설명하고 있는 점이 있다면 언제든 댓글 달아주세요.)
결론부터 말씀드리면,
두 개념(Blocking & Non-Blocking / Synchronous & Asynchronous)는
각각 제어권과 결과값을 초점으로 두고있습니다.
글만으로는 이해가 안될 것 같아 예제 코드를 가져와봤습니다.
예제 코드는 이름과 나이로 player를 찾고,
(중복은 없다고 가정하겠습니다.)
해당 player가 성인이라면 player를 return하는 간단한 코드입니다.
Blocking / Non-Blocking
public Player getAdultPlayer(String name, int age) throws InterruptedException {
boolean isAdult = isAdultSync(age);
Player player = playerRepository.findAllByNameAndAgeWithDelay(name, age);
if (isAdult) {
return player;
}
return null;
}
위 코드에서 findAllByNameAndAgeWithDelay method는 DB에서 player를 찾습니다.
JDBC의 내부를 들여다보면 InputStream의 read() method를 이용해
결과값을 가져오는 것을 알 수 있는데,
read() method는 결과값을 가져오거나 Exception이 발생할때까지
thread를 멈추게(blocking)합니다.
blocking 된 thread는 wait queue에 들어가게 되고,
결과값을 가져올때까지 대기하게 됩니다.
이를 도식화하면 다음과 같습니다.
즉, thread는 제어권을 kernel에게 넘겨주고
kernel의 callback을 호출하여 제어권을 다시 넘겨주면
이후 로직을 수행할 수 있는 것입니다.
그렇다면 다음 예제는 어떻게 동작할까요?
(Non-Blocking 예제는 위 동일한 로직을 구현하기가 어려워,
적절한 예제 코드로 대체하였습니다.)
device = IO.open()
ready = False
while not ready:
//읽을 준비가 되었는지 확인
ready = IO.poll(device, IO.INPUT, 5)
data = device.read()
print(data)
위 예제코드는 파일에서 읽어 온 값을 출력하는 코드입니다.
여기서 첫번째 줄의 open 함수는 Non-Blocking 방식으로 동작하여
while문 내부에서 파일을 읽을 준비가 되었는지
polling 방식으로 확인합니다.
(polling 방식이란, 주기적으로 상태를 확인하는 방식을 의미합니다.)
해당 내용 역시 도식화하면 다음과 같습니다.
즉, thread는 wait queue에 들어가지 않고
계속해서 data를 읽을 수 있는지 확인하고
읽을 준비가 되면 data를 읽어 옵니다(busy-wait).
Synchronous / Asynchronous
이번에는 Synchronous와 Asynchronous를 살펴보겠습니다.
public Player getAdultPlayer(String name, int age) throws InterruptedException {
boolean isAdult = isAdultSync(age);
Player player = playerRepository.findAllByNameAndAgeWithDelay(name, age);
if (isAdult) {
return player;
}
return null;
}
private boolean isAdultSync(int age) throws InterruptedException {
Thread.sleep(3000);
return age > 18;
}
기존 예제 코드로 돌아와서
우리는 blocking 방식으로 player의 정보를 가져왔습니다.
하지만 그 전에 isAdultSync 함수를 통해
해당 player가 성인인지를 확인합니다.
이때, 우리는 isAdultSync의 응답값이 오기전까지
다음 로직을 수행하지 않습니다.
DB에서 player 정보를 가져오는데 걸리는 시간이 5초라고 가정한다면
위 로직이 수행되는데에 걸리는 시간은 어떻게 될까요?
별로 어렵지 않게 8초가 걸리는 것을 알 수 있습니다.
다음 코드는 위와 동일한 로직을 Asynchronous 방식으로 구현한 것입니다.
public Player getAdultPlayer(String name, int age) throws InterruptedException, ExecutionException {
Future<Boolean> isAdult = isAdultAsync(age);
Player player = playerRepository.findAllByNameAndAgeWithDelay(name, age);
if (isAdult.get()) {
return player;
}
return null;
}
private Future<Boolean> isAdultAsync(int age) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
return executorService.submit(() -> {
Thread.sleep(3000);
return age > 18;
});
}
isAdultAsync 메소드는 이름에서 알 수 있듯이 Asynchronous하게 동작합니다.
위 메소드를 호출하고 응답값으로 받은 isAdult에는
결과값이 들어있지 않습니다.
(여기서 응답값은 메소드에서 return되는 값,
결과값은 player가 성인인지 판별하는 true/false 값을 의미합니다.)
실제 로직은 별도의 thread에서 동작하며
isAdult.get 메소드를 통해 결과값을 가져오게 됩니다.
즉, Asynchronous는 결과값을 기다리지 않고
다음 로직을 수행할 수 있습니다.
위 예제에서도 DB에서 player 정보를 가져오는데 걸리는 시간이 5초라고 가정한다면,
Synchronous와는 다르게 총 5초의 시간이 걸리게 됩니다.
다음 글에서는 조금 더 나아가
Blocking & Synchronous, Blocking & Asynchronous,
Non-Blocking & Synchronous, Non-Blocking & Asynchronous
각각의 케이스에 대해 다뤄보겠습니다.
Appendix
위 예제에서 DB 쿼리 수행 중 JVM thread의 상태를 보면
BLOCKED가 아니라 Running 상태인 것을 알 수 있습니다.
하지만 이는 thread가 실제 running 중인 것이 아니라
JVM의 레벨에서는 OS 레벨의 thread 상태를 정확하게 알 수 없기 때문입니다.
아래 내용은 Thread.java에 포함된 running(RUNNABLE) 상태의 설명입니다.
Thread state for a runnable thread. A thread in the runnable state is executing in the Java virtual machine but it may be waiting for other resources from the operating system such as processor.
Reference
- velog.io/@codemcd/Sync-VS-Async-Blocking-VS-Non-Blocking-sak6d01fhx
- developer.ibm.com/articles/l-async/
- https://howtodoinjava.com/java/io/how-java-io-works-internally/
'전체보기 > Computer science' 카테고리의 다른 글
Parallelism(병렬성)과 Concurrency(동시성) (0) | 2021.01.06 |
---|