[Reacive Programming] Blocking vs Non-Blocking, Sync vs Async

DaeHoon·2023년 5월 21일
0
post-thumbnail

1. Blocking Vs Non-Blocking

  • Blocking: 자신의 작업을 진행하다가 다른 주체의 작업이 시작되면 다른 작업이 끝날 때까지 기다렸다가 자신의 작업을 시작함
  • Non-Blocking: 다른 주체의 작업에 관련없이 자신의 작업을 진행함
  • 간단하게 정리하면 다른 주체가 작업을 할 때 자신의 제어권이 있는지 없는지로 볼 수 있다.

Example

  • 블로킹 방식은 상사가 서류를 읽어볼테니 기다리라고 하는 상황
  • 논블로킹 방식은 상사가 서류를 받고 읽어볼테니 자리로 돌아가라서 볼일 보라고 하는 상황

2. Sync Vs. Async

  • Synchronous: 동기. 작업을 동시에 수행하거나, 동시에 끝나거나, 끝나는 동시에 시작함을 의미
  • Asynchronous: 비동기. 시작, 종료가 일치하지 않으며, 끝나는 동시에 시작을 하지 않음을 의미
  • 간단하게 정리하면 결과를 돌려줄 때 순서와 결과에 관심이 있는지 아닌지로 판단할 수 있다.

Example

  • 동기 방식은 상사에게 서류를 전달하고 지시를 내릴 때 즉시 일을 처리하는 방식. 이 때 직원은 상사를 기다리거나 다른 일을 할 수 있다.
  • 비동기 방식은 상사에게 서류를 전달하고 지시를 내리면 언젠간 처리하는 방식. 마찬가지로 직원은 상사를 기다리거나 다른 일을 할 수 있다.

3. 4가지 조합

3-1. Blocking / Sync

  • 일반적인 동기 함수의 동작 방식. 함수가 다른 함수를 호출하고, 그 결과를 반환받아서 다음 작업을 이어나감
  • 자바에서 입력 요청을 할 때 이 방식을 사용한다. 입력이 요청될 때 까지 그 다음 코드의 내용은 동작하지 않는다.

3-2. Non-Blocking / Sync

  • 논블로킹이기 때문에 제어권은 바로 받게 된다. 동기적인 방식이기 때문에 호출한 함수의 작엽 완료 여부, 결과값에 중점을 둠. 그래서 논 블로킹임에도 불구하고 호출된 함수의 완료 여부를 계속해서 문의하게 된다.
  • 게임에서 맵을 넘어갈 때를 예시로 들 수 있다. 맵의 데이터를 가져올 때 까지 유저의 로드율을 클라이언트에 보여줘야 할 필요가 있기 때문.

3-3. Blocking / Async

  • 호출된 함수의 결과에는 관심이 없지만, 제어권을 가지지 못하기 때문에 호출된 함수의 작업이 완료될 때까지 대기하게 된다.

  • 이점이 없는 방식이기 때문에 잘 구현하지 않는다. 주로 Async-NonBlocking 구조를 구현하려다가 제어권을 잘 반환하지 못해서 Async-Blocking 으로 동작하는 경우가 종종 있다.

3-4. Non-Blocking / Async

  • 일반적인 비동기 함수의 동작 방식. 함수가 다른 함수를 호출하고 바로 제어권을 받는다. 제어권을 반환 받은 함수는 다른 함수의 결과값에 상관없이 다른 작업을 수행할 수 있다.

Example

  • Blocking / Sync: 상사에게 서류를 전달하러 갔을 때, 상사의 지시가 내려지기까지 대기하다가 지시를 받으면 즉시 처리하는 경우
  • Non-Blocking / Sync: 상사에게 서류를 전달하러 갔지만, 상사에게 계속 결과가 나왔냐고 물어보고 있는 상태. 결과가 나오면 바로 받아서 일을 처리하는 경우
  • Blocking / Async: 상사에게 서류를 전달하러 갔는데, 상사의 지시가 내려지기까지 대기하다가 지시를 받으면 여유가 되는 시간에 처리하는 경우
  • Non-Blocking / Async: 상사에게 서류를 전달하러 갔을 때, 상사는 읽어볼테니 돌아가라고 직원에게 얘기한다. 직원은 돌아가서 자기 할일을 하는 경우

3. 함수 호출 관점에서 동기와 비동기

Caller와 Callee

  • 함수가 다른 함수를 호출하는 상황
    • Caller: 호출하는 함수
    • Callee: 호출 당하는 함수

함수형 인터페이스

  • 함수형 프로그래밍을 지원하기 위해 java 8부터 도입
  • 1개의 추상 메서드를 가지고 있는 인터페이스
  • 함수를 1급 객체로 사용할 수 있다.
@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
@FunctionalInterface
public interface Supplier<T> {
    T get();
}
@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
  • Function, Consumer, Supplier, Runnable 등
  • 함수형 인터페이스를 구현한 익명 클래스를 람다식으로 변경 가능

Example

public static void main(String[] args) {
    var consumer = getConsumer();
    consumer.accept(1);
    var consumerAsLambda = getConsumerAsLambda();
    consumerAsLambda.accept(1);
    handleConsumer(consumer);
}
// 익명 클래스로 구현
public static Consumer<Integer> getConsumer() {
    Consumer<Integer> returnValue = new Consumer<Integer>() {
        @Override
        public void accept(Integer integer) {
            log.info("value in interface: {}", integer);
        }
    };
    return returnValue;
}
// 익명클래스 대신 람다 식으로 구현
public static Consumer<Integer> getConsumerAsLambda() {
    return integer -> log.info("value in lambda: {}", integer);
}

public static void handleConsumer(Consumer<Integer> consumer) {
    log.info("handleConsumer");
    consumer.accept(1);
}
output
29:47 [main] - value in interface: 1
29:47 [main] - value in lambda: 1
29:47 [main] - handleConsumer
29:47 [main] - value in interface: 1

네 가지의 모델

1) A 모델 (동기, Blocking)

@Slf4j
public class A {
    public static void main(String[] args) {
        log.info("Start main");
        var result = getResult();
        var nextValue = result + 1;
        assert nextValue == 1;
        log.info("Finish main");
    }
    public static int getResult() {
        log.info("Start getResult");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        var result = 0;
        try {
            return result;
        } finally {
            log.info("Finish getResult");
        }
    }
}
output
22:47:30.075 [main] INFO -- Start main
22:47:30.077 [main] INFO -- Start getResult
22:47:31.081 [main] INFO -- Finish getResult
22:47:31.082 [main] INFO -- Finish main
  • maingetResult의 결과 값에 관심이 있다.
  • main은 결과를 이용해서 다음 코드를 실행한다.

2) B 모델 (비동기, Blocking)

public class B {
    public static void main(String[] args) {
        log.info("Start main");
        getResult(new Consumer<Integer>() {
            @Override
            public void accept(Integer integer) {
                var nextValue = integer + 1;
                assert nextValue == 1;
            }
        });
        log.info("Finish main");
    }
    public static void getResult(Consumer<Integer> cb) {
        log.info("Start getResult");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        var result = 0;
        cb.accept(result);
        log.info("Finish getResult");
    }
}
output 
22:47:43.871 [main] INFO -- Start main
22:47:43.873 [main] INFO -- Start getResult
22:47:44.878 [main] INFO -- Finish getResult
22:47:44.879 [main] INFO —- Finish main
  • maingetResult의 결과에 관심이 없다
  • getResult는 결과를 이용해서 함수형 인터페이스를 실행한다

Sync Vs. Async

  • 동기 (A 모델)
    • caller는 결과를 이용해서 action을 수행한다
    • callercallee의 결과에 관심이 있다
  • 비동기 (B 모델)
    • callercallee의 결과에 관심이 없다
    • callee는 결과를 이용해서 callback을 수행한다

A와 B의 공통점

  • 동기 (A 모델)
    • maingetResult가 완료될 때까지 대기한다
    • maingetResult가 결과를 돌려주기 전까지 아무것도 할 수 없다
  • 비동기 (B 모델)
    • maingetResult가 결과를 구하고 callback을 실행하기 전까지 아무것도 할 수 없다
    • maingetResult가 완료될 때까지 대기한다

Blocking

  • callee를 호출한 후, callee가 완료되기 전까지 caller가 아무것도 할 수 없다
  • 제어권callee가 가지고 있다

3) C 모델 (동기, Non-blocking)

public static void main(String[] args)
        throws InterruptedException, ExecutionException {
    log.info("Start main");
    var count = 1;
    Future<Integer> result = getResult();
    while (!result.isDone()) {
        log.info("Waiting for result, count: {}", count++);
        Thread.sleep(100);
    }
    var nextValue = result.get() + 1;
    assert nextValue == 1;
    log.info("Finish main");
}
   
public static Future<Integer> getResult() {
    var executor = Executors.newSingleThreadExecutor();
    try {
        return executor.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                log.info("Start getResult");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                var result = 0;
                try {
                    return result;
                } finally {
                    log.info("Finish getResult");
                }
            }
        });
    } finally {
        executor.shutdown();
    }
}   
output
47:06 [main] - Start main
47:06 [pool-2-thread-1] - Start getResult
47:06 [main] - Waiting for result, count: 1
47:06 [main] - Waiting for result, count: 2
47:07 [main] - Waiting for result, count: 3
47:07 [main] - Waiting for result, count: 4
47:07 [main] - Waiting for result, count: 5
47:07 [main] - Waiting for result, count: 6
47:07 [main] - Waiting for result, count: 7
47:07 [main] - Waiting for result, count: 8
47:07 [main] - Waiting for result, count: 9
47:07 [main] - Waiting for result, count: 10
47:07 [pool-2-thread-1] - Finish getResult
47:07 [main] - Finish main
  • getResult를 호출한 후, getResult가 완료되지 않더라도 main은 본인의 일을 할 수 있다.

Non-Blocking

  • callee를 호출한 후, callee가 완료되지 않더라도 caller는 본인의 일을 할수 있다
  • 제어권을 caller가 가지고 있다

Blocking Vs Non-Blocking

  • Blocking
    • callercallee가 완료될 때 까지 대기한다.
    • 제어권callee가 가지고 있다
    • caller다른 별도의 thread가 필요하지 않다 (혹은 thread를 추가로 쓸 수도 있다)
  • Non-Blocking
    • callercallee기다리지 않고 본인의 일을 한다.
    • 제어권caller가 가지고 있다
    • caller다른 별도의 thread가 필요하다

4) D 모델 (비동기, Non-blocking)

ublic static void main(String[] args) {
    log.info("Start main");
    getResult(new Consumer<Integer>() {
        @Override
        public void accept(Integer
                                   integer) {
            var nextValue = integer + 1;
            assert nextValue == 1;
        }
    });
    log.info("Finish main");
}

public static void getResult(Consumer<Integer> callback) {
    var executor = Executors.newSingleThreadExecutor();
    try {
        executor.submit(new Runnable() {
            @Override
            public void run() {
                log.info("Start getResult");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                var result = 0;
                try {
                    callback.accept(result);
                } finally {
                    log.info("Finish getResult");
                }
            }
        });
    } finally {
        executor.shutdown();
    }
}
[main] INFO -- Start main
[pool-1-thread-1] INFO -- Start getResult
[main] INFO -- Finish main
[pool-1-thread-1] INFO -- Finish getResult
  • maingetResult의 결과에 관심이 없다
    • 비동기이다
  • getResult를 호출한 후, getResult가 완료되지 않더라도 main은 본인의 일을 할 수 있다
    • Non-Blocking
  • 즉, D 모델은 비동기 non-blocking 모델

정리

4. Blocking의 전파

  • blockingthread오랜 시간 일을 하거나 대기하는 경우 발생
  • CPU-bound blocking: 오랜 시간 일을 한다
  • IO-bound blocking: 오랜 시간 대기한다

CPU-bound blocking

  • thread가 대부분의 시간 CPU 점유
  • 연산이 많은 경우
  • 추가적인 코어를 투입

IO-bound blocking

  • thread가 대부분의 시간을 대기
  • 파일 읽기/쓰기, network 요청 처리, 요청 전달 등
  • IO-bound non-blocking 가능

Reference

https://www.youtube.com/watch?v=oEIoqGd-Sns&ab_channel=%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC
Sprin gWebflu x완전정복:코루틴부터 리액티브 MSA 프로젝트까지 (FastCampus)

blocking의 전파


• 하나의 함수에서 여러 함수를 호출하기도 하고, 함수 호출은 중첩적으로 발생
calleecaller가 되고 다시 다른 callee를 호출
blocking한 함수를 하나라도 호출한다면 callerblocking이 된다


• 함수가 non-blocking하려면 모든 함수가 non-blocking이어야 한다
• 따라서 I/O bound blocking 또한 발생하면 안된다

profile
평범한 백엔드 개발자

0개의 댓글