개인공부/컴퓨터 구조와 운영체제

Blocking & NonBlocking , Synchronous & Asynchronous

개바리여리 2025. 8. 14. 09:10

 

Blocking

요청을 보내면 결과가 반환되기까지 멈춰서 기다린다.

블로킹 상태에서는 해당 스레드는 다른 작업을 하지 못한다.

 

예를 들어 컴퓨터에서 프린터로 프린팅 작업을 요청 보내고 기다리는 것을 예시로 들 수 있다.

 

프린터에 출력 요청을 보내면, 운영체제는 해당 요청을 프린터 드라이버를 통해 프린터로 전달하고, 프린터가 작업을 시작할 때까지 대기합니다. 이 과정에서 프로그램은 다음 작업을 수행하지 못하고 멈춰있게 된다.

 

프린터의 경우, 대부분의 프린터 드라이버는 내부적으로 큐(Queue)를 사용하여 여러 출력 요청을 순서대로 처리하지만, 애플리케이션의 관점에서는 프린터가 요청을 수락하고 작업을 시작할 때까지는 Blocking 상태가 된다.

 

NonBlocking

요청을 보내면 결과가 준비되지 않아도 해당 스레드에게 제워권을 다시 돌려준다.

호출 즉시 다음 작업을 수행가능하다.

 

기존의 블로킹 방식의 프린터 출력을 별도의 스레드를 사용하여 메인 프로그램을 블로킹하지 않거나, 프린터 요청을 별도로 처리하는 에이전트 프로그램을 통해 다른 프로세스에서 처리하도록 함으로써 애플리케이션의 메인 스레드 블로킹을 막을 수 있다.

 

 

소켓 관점에서 블로킹&논블로킹

 

모든 소켓에는 커널 공간에 수신버퍼와 송신버퍼가 존재한다.

 

  • read/recv는 RecvQ에서 이미 도착해 쌓여 있는 바이트를 유저 공간으로 복사한다.
  • write/send는 유저 공간의 바이트를 SendQ로 복사해두고 커널/네트워크 스택이 나중에 전송한다.

기본적으로 소켓은 Blocking이다.

 

<Blocking>

recv()

  • RecvQ에 데이터가 없으면 데이터가 도착할 때까지(혹은 타임아웃까지) 대기.
  • 도착한 만큼만 읽혀서 리턴(부분 읽기 가능).
  • 리턴값 0이면 상대가 정상 종료(EOF).

send()

  • SendQ에 빈 공간이 없으면 공간이 생길 때까지 대기.

<Non-Blocking>

recv()

  • 읽을 데이터가 없으면 즉시 -1과 EAGAIN/EWOULDBLOCK(윈도우는 WSAEWOULDBLOCK) 반환.

send()

  • SendQ가 꽉 차 있으면 즉시 -1과 EAGAIN 반환

시도한다 → 2) 못 하면(EAGAIN) 이벤트 대기 → 3) 준비 신호(readable/writable) 오면 다시 시도한다.
(읽기는 데이터가 떨어질 때까지 루프, 쓰기는 버퍼가 빌 때까지 루프)

 

논블로킹일때 작업완료 확인에는 보통 폴링방식을 사용하지 않음 => CPU를 너무 태운다. 비효율적이다

 

* epoll (Linux), kqueue (BSD/macOS)

SELECT의 단점을 보완하여 사용할 수 있도록 만든 IO통지 모델. (커널 수준에서 관리됨)

 

  • epoll_create로 이벤트 관리자 만들기
  • epoll_ctl로 “이 소켓 상태 알려줘” 등록
  • epoll_wait에서 잠들었다가 이벤트 있으면 깨어남

 

 

* IOCP (Windows)

소켓이나 파일의 입출력을 최소한의 스레드로 사용하여 처리하는 기법

스레드풀링이나 비동기 처리등을 운영체제에서 관리 

간단히 말해서 "Windows 운영 체제에서 비동기 입출력 작업을 처리하기 위한 메커니즘"

I/O 요청을 등록하면 커널이 완료되었을 때 Completion Queue에 알림

 

Synchronous

작업이 끝나는 시점에 맞춰 순차적으로 실행한다.

동작한 함수가 결과를 직접 받아야만 다음로직을 실행한다. (치킨 시키고 직접 가게에 전화해서 도착했냐고 묻는것.)

호출자가 결과를 직접 챙긴다.

 

식당에서 주문을 할 때, 한 손님의 주문을 받고 음식이 나올 때까지 다음 손님의 주문을 받지 않는 경우. 첫 번째 손님의 주문을 처리하고, 음식이 만들어져서 나올 때까지 기다렸다가 다음 손님의 주문을 받는 방식

 

Asynchronous

작업의 완료를 기다리지 않고 다른작업을 진행한다.

결과가 준비되면 콜백함수 or 이벤트를 통해 알려주거나 처리하게 된다. (실행순서를 지키지 않아도 된다.)

 

카페에서 주문을 할 때, 카운터에서 주문을 하고 결제를 한 후 자리에 앉아 기다리는 경우. 직원은 주문을 받고 결제를 한 후 바로 다음 손님의 주문을 받습니다. 내 음료가 준비되면 직원이 번호를 부르거나 알림 벨을 울려 음료가 완성되었음을 알려줍니다. 즉, 주문을 하고 기다리는 동안 다른 작업을 할 수 있습니다.

 

 

  • Synchronous: A 작업 완료 → B 작업 시작 (순차적)
  • Asynchronous: A 작업 시작 → B 작업 시작 → A 작업 완료 알림 (비순차적, 동시적)

Blocking&Non-Blocking 과 Synchronous & Asynchronous 는 

"호출하는 순간 멈추냐 안멈추냐" 와 "결과를 직접 받느냐 나중에 알려주느냐" 에 대한 관심사 차이이다.

 

** Non-Blocking 과 Async 는 둘다 기다리지 않고 다른일을 처리한다는 느낌에서는 같은 개념이라고 생각할 수 있지만,

 

NonBlocking은 호출자의 관점 (실행흐름이 막히냐 안막히느냐),

- 음식점에서 주문했는데 직원이 "주문 접수 완료" 라고 하며 나를 바로 보내준다. (나는 실행흐름이 막히지 않았다.)

하지만 음식이 준비되었는지는 내가 계속 확인해야 한다. 

 

Async는 작업/결과의 관점으로 (나중에 끝나냐) 보면된다. ---> 즉 그냥 다른 개념, 관점이 다름.

- 음식이 준비되면 알아서 전화/알림을 준다. 나는 기다릴 필요없이 다른 일 하고 있다가 알림받고 먹으면 된다.

 

 

Blocking + Syc

String data = readFile("data.txt"); // 파일 다 읽을 때까지 대기
System.out.println(data);

 

 

Blocking + Asynchronous

 

Future<String> f = executor.submit(task);
String result = f.get(); // 여기서 블로킹 → 작업 끝날 때까지 대기



// 다른 예시

public class BlockingAsyncExample {
    public static void main(String[] args) throws Exception {
        System.out.println("작업 시작");

        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<String> future = executor.submit(() -> {
            Thread.sleep(2000);
            return "완료";
        });

        // 다른 작업 가능
        System.out.println("다른 작업 실행 중...");

        // 하지만 get()에서 블로킹됨
        String result = future.get(); 
        System.out.println("결과: " + result);

        executor.shutdown();
    }
}

 

백그라운드에서 작업을 하긴 하는데, 호출자는 결과 받을 때까지 기다려야 하니 **사실상 비동기의 장점(동시에 다른 일 수행)**이 거의 사라짐

 

 

 

Non-blocking + Async

readFileAsync("data.txt", result -> {
    System.out.println(result); // 다 읽으면 콜백 실행
});
System.out.println("파일 읽는 동안 다른 작업 실행 가능");


//다른 예제
public class NonBlockingAsyncExample {

    public static void main(String[] args){
        System.out.println("작업 시작");

        CompletableFuture.supplyAsync(() -> {
            try{
                Thread.sleep(2000);
            }catch(InterruptedException e){

            }
           return "완료";
        }).thenAccept(result -> {  //비동기
            System.out.println("결과 : " + result);
        });

        //호출자 블로킹 안됨
        System.out.println("다른 작업 실행중");

        try { Thread.sleep(3000); } catch (InterruptedException e) {}
        System.out.println("작업 종료");
    }
}

 

 

Non-Blocking + Synchronous

import java.util.concurrent.*;

public class NonBlockingSyncExample {
    public static void main(String[] args) throws Exception {
        System.out.println("작업 시작");

        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<String> future = executor.submit(() -> {
            Thread.sleep(2000);
            return "완료";
        });

        // Non-blocking: 바로 반환됨 (future 객체 받음)
        System.out.println("다른 작업 실행 중...");

        // Sync Polling: 결과가 나왔는지 계속 확인해야 함
        while (!future.isDone()) {
            System.out.println("아직 완료 안됨...");
            Thread.sleep(500);
        }

        System.out.println("결과: " + future.get());
        executor.shutdown();
    }
}