Blocking과 Non-Blocking, Sync와 Async는 비슷한 부분이 많지만 어떤 것에 관심사를 두고 있는지에 따라 구분되는 개념이기 때문에 헷갈릴 수 있고 구분하기 어려운 개념 중 하나이다.
그래서 각각에 대한 개념과 차이점을 알아보며 정리를 해보겠다.
Blocking & Non-Blocking
Blocking과 Non-Blocking의 관심사는 제어권의 반환에 있다.
즉 제어할 수 없는 대상을 어떻게 처리하는가? 에 관한 문제이다.
먼저 그림을 살펴보자
Blocking
호출된 스레드가 자신의 작업을 모두 마칠 때까지 호출한 스레드에게 제어권을 넘겨주지 않고 대기하게 만드는 것을 말한다.
Non-Blocking
호출된 스레드가 작업을 마치지 않더라도 호출한 스레드에게 제어권을 넘겨주어 호출한 스레드가 다른 일을 할 수 있도록 하는 것을 말한다.
Sync & Async
Synchronous와 Asynchronous의 관심사는 작업 완료 여부를 누가 신경쓰느냐에 있다.
Sync와 Async는 단순히 생각했을 때 Blocking & Non-Blocking과 차이점을 구별하기가 쉽지 않다. 이 개념은 앞선 두 개념에 비해 추상적인 개념이기 때문에 해석의 여지가 생기기 때문이다.
Synchronous의 어원은 Syn(함께) + Chrono(시간), 즉 시간을 함께 맞춘다 라는 뜻이라고 한다.
그럼 어떤 시간을 맞추는 것을 말하는 것인지 알아보자.
Sync
Sync는 스레드 A가 스레드 B를 실행시켰을 때 스레드 B의 결과를 스레드 A가 지속적으로 물어보는 것을 말한다.
즉 호출자가 피호출자의 결과를 계속해서 기다린다는 뜻이다.
Async
Sync와는 반대로 Async는 스레드 B의 결과를 스레드 A가 신경쓰지 않기 때문에 스레드 B가 끝마친 결과를 스레드 A에게 가져다고 볼 수 있다.
Sync + NonBlocking
위의 그림은 Non-Blocking & Sync를 설명한 개념이다.
스레드 A가 스레드 B를 실행시킨 후 제어권을 가져와(Non-Blocking) 자신의 작업을 수행하면서도 스레드 B에게 지속적으로 완료 여부를 요청(Sync)하게 된다.
이때 스레드 B가 만약 작업이 완료되지 않은 상태라면 "완료되지 않음"이라는 결과를 반환하는 것이다.
자바에서는 이 작업을 Future, CompletableFuture, ListenableFuture 을 통해 지원한다.
Future future = asyncFileChannel.read(~~~);
while(!future.isDone()) {
// isDone()은 asyncChannle.read() 작업이 완료되지 않았다면 false를 바로 리턴해준다.
// isDone()은 물어보면 대답을 해줄 뿐 작업 완료를 스스로 신경쓰지 않고,
// isDone()을 호출하는 쪽에서 계속 isDone()을 호출하면서 작업 완료를 신경쓴다.
// asyncChannle.read()이 완료되지 않아도 여기에서 다른 작업 수행 가능
}
// 작업이 완료되면 작업 결과에 따른 다른 작업 처리
위 코드처럼 future.isDone() 메소드를 통해 작업이 끝났는지 여부를 알 수 있다.
Async + Blocking
스레드 A가 스레드 B를 실행시키고 스레드함수 B의 작업이 끝날 때까지 기다리며(Blocking), 작업이 끝나면 스레드 B가 그 결과를 직접 반환하게 된다.
간혹 Async + Non-Blocking를 잘못 처리하여 위와 같은 방식으로 처리해 성능에 악영향을 줄 수 있다고 한다.
또는 Async + Non-Blocking방식을 사용하는데 그 과정 상 하나라도 Blocking으로 동작하는 부분이 있으면 의도치 않게 Blocking + Async로 동작할 수 있다고 한다.
예시로 Java의 JDBC에서 DB 작업 호출 시 MySQL이 제공하는 드라이버를 사용하는데, 이 드라이버가 Blocking 방식이라고 한다.
하지만 이 상황에서는 스레드 A가 대기를 하고 있기 때문에 사실 Sync + Blocking과 성능적인 차이가 없어 거의 사용하지 않는다.
하지만..
하지만 이 네가지 개념이 모든 상황에서 뚜렷하게 나눌 수 없다.
다음의 코드를 살펴보자.
factorial(number) 메소드는 멀티 스레드로 비동기/논블로킹 방식으로 작동된다.
futureTask.isDone()메소드 자체도 아주 찰나의 순간이지만 호출하는 순간 잠시 멈추어 다른 스레드의 결과를 받아오게 되므로 동기라 할 수 있다.
System.out.println(...)메소드는 factorial(number)가 실행되는 동안 실행이 되므로 비동기 방식으로 작동한다.
futureTask.get()메소드도 마찬가지로 결과를 받아올 때까지 잠시 멈추어 직접 결과를 받아오므로 동기/블로킹 방식이라 할 수 있다.
하지만 코드 전체적으로 봤을 때 futureTask가 끝난 후 반복문에서 벗어나 futureTask.get()메소드를 호출하기 때문에 이는 동기라 볼 수 있다.
이처럼 내가 바라보는 관점에 따라 하나의 코드 진행에서도 이를 네가지 개념으로 명확히 나누기는 쉽지 않다.
정리
위의 개념들을 명확히 구분하기란 이처럼 쉽지 않다.
하지만 개념적으로 큰 범주로써 나누자면 다음과 같이 정리할 수 있을 것 같다.
Blocking, Non-Blocking: 제어권을 누가 갖는가?
- Blocking: 호출한 쪽이 제어권을 갖는다
- Non-Blocking: 호출 당한 쪽이 제어권을 갖는다.
Sync, Async: 호출되는 작업의 완료 여부를 누가 신경쓰느냐?
- Sync: 호출한 쪽이 작업 여부를 신경써서 지속적으로 완료 여부를 요청한다.
- Async: 호출 당한 쪽이 자신의 완료 여부를 직접 반환한다.