hyeonga_code

Sync/Async Blocking/Non-Blocking 가 대체 뭐야? 이해하기 본문

Project_HYEONGARL

Sync/Async Blocking/Non-Blocking 가 대체 뭐야? 이해하기

hyeonga 2024. 6. 27. 05:59
반응형

혹시 이상한 부분이 있다면 꼭 문제를 제기해줬으면 한다...... 제발..부탁..

 

시작.

sync/async 와 blocking/non-bloking에 대해 알아보기로 했다.
가지고 있는 Java 관련 기초 개념서에는 자세하게 나와있지 않아 구글링으로 시작했다.


대부분의 블로그에서 볼 수 있는 내용이다.

Sync /Async

호출된 함수의 종료를 호출한 함수가 처리하는지 호출된 함수가 처리하는지에 대한 기준으로 나뉜다.

더보기

Sync : 동기 처리


A 가 B 를 호출할 때 B 의 결과를 A가 처리한다.
요청 순서대로 반환된다.
A, B 모두 결과와 순서를 중요하게 생각한다.


작업을 순차적으로 처리하는 방식으로 하나의 작업이 완료되어야 다음 작업을 실행한다.
순차적이고 직렬적으로 작업이 처리되므로 처리속도가 느릴 수 있다.
단순한 파일 읽기/쓰기, 단일 작업 수행이 필요한 상황에 사용

1. 호출과 대기
함수를 호출하면 그 함수가 모든 작업을 완료하고 결과를 반환할 때까지 호출한 함수는 대기한다.

2. 순차적 실행
작업이 순차적으로 진행되며 한 작업이 끝나야 다음 작업이 시작한다.

3. 단일 흐름
프로그램 실행의 흐름이 단일 스레드에서 일어나 한 번에 하나의 작업만 처리된다.

 

import org.junit.jupiter.api.Test;

import java.net.HttpURLConnection;
import java.net.URL;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class SyncTest {

    @Test
    void 동기_파일_읽기() {
        try {
            // 동기적으로 파일 읽기
            String content = new String(Files.readAllBytes(Paths.get("test.txt")));
            System.out.println(content);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    @Test
    void 순차적_네트워크_요청() {
        try {
            // 첫 번째 동기 네트워크 요청
            System.out.println("첫 번째 네트워크 요청");
            URL url1 = new URL("http://example.com");
            HttpURLConnection conn1 = (HttpURLConnection) url1.openConnection();
            conn1.setRequestMethod("GET");
            BufferedReader in1 = new BufferedReader(new InputStreamReader(conn1.getInputStream()));
            String inputLine1;
            StringBuilder content1 = new StringBuilder();
            while ((inputLine1 = in1.readLine()) != null) {
                content1.append(inputLine1);
            }
            in1.close();
            System.out.println("Response from first request: " + content1.toString());

            // 두 번째 동기 네트워크 요청
            System.out.println("두 번째 네트워크 요청");
            URL url2 = new URL("http://example.com");
            HttpURLConnection conn2 = (HttpURLConnection) url2.openConnection();
            conn2.setRequestMethod("GET");
            BufferedReader in2 = new BufferedReader(new InputStreamReader(conn2.getInputStream()));
            String inputLine2;
            StringBuilder content2 = new StringBuilder();
            while ((inputLine2 = in2.readLine()) != null) {
                content2.append(inputLine2);
            }
            in2.close();
            System.out.println("Response from second request: " + content2.toString());

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

Async : 비동기 처리


A 가 B 를 호출할 때 B 의 결과를 B 가 처리한다.(CallBack)
요청 순서대로 반환되지 않는다.
B 가 결과를 직접 처리한다.

 

작업을 동시에 처리하는 방식으로 하나의 작업이 완료되지 않아도 다음 작업을 계속해서 진행할 수 있다.
콜백함수나 프로미스와 같은 방법을 사용하여 작업이 완료되면 결과를 처리할 수 있다.
병렬적으로 작업을 처리하여 처리 속도를 향상시킬 수 있다.
네트워크 요청, 대규모 파일 처리, 병렬 작업이 필요한 경우

1. 호출과 반환
작업을 요청하면 바로 반환되고 작업은 백그라운드에서 진행된다.


2. 콜백
작업이 완료되면 결과를 처리할 콜백 함수가 호출되거나 프로미스/퓨처와 같은 객체를 통해 결과가 전달된다.


3. 다른 작업 수행
비동기 작업이 진행되는 동안 호출자는 다른 작업을 수행할 수 있다.

 

import org.junit.jupiter.api.Test;

import java.nio.file.*;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.ByteBuffer;
import java.io.IOException;
import java.util.concurrent.Future;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.util.concurrent.CompletableFuture;

public class AsyncTest {

    @Test
    void 비동기_파일_읽기() {
        try {
            AsynchronousFileChannel channel
                    = AsynchronousFileChannel.open(Paths.get("test.txt"), StandardOpenOption.READ);
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            Future<Integer> operation = channel.read(buffer, 0);

            while (!operation.isDone()) {
                System.out.println("Reading file...");
            }

            buffer.flip();
            byte[] data = new byte[buffer.limit()];
            buffer.get(data);
            System.out.println(new String(data));
            buffer.clear();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Test
    void 비동기_네트워크_요청() {
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("http://example.com/data"))
                .build();

        CompletableFuture<HttpResponse<String>> future
                = client.sendAsync(request, HttpResponse.BodyHandlers.ofString());

        // 비동기 작업이 완료될 때까지 다른 작업 수행 가능
        future.thenApply(HttpResponse::body)
                .thenAccept(System.out::println)
                .join(); // 블로킹하여 결과를 기다릴 경우 사용
    }
}

 

Blocking/Non-Blocking

호출된 함수가 호출한 함수에게 제어권을 바로 주는지로 나뉜다.

더보기

Blocking :

A 가 B 를 호출할 때, B 가 자신의 작업이 종료되기 전까지 A 에게 제어권을 돌려주지 않는다.
호출된 함수가 완료될 때까지 제어권을 가지고 있다.
간단한 입출력 작업, 동기 처리 방식의 기본

1. 호출 및 대기
특정 함수나 메소드가 호출되면 그 함수는 결과를 반환할 때까지 호출한 곳에서 대기한다.


2. 자원 소모
블로킹 상태에서는 대기하는 동안에도 CPU 자원을 사용하므로 자원의 비효율성이 발생할 수 있다.

 

3. 동기적 처리
작업이 완료될 때까지 다음 코드나 작업이 실행되지 않는다.

 

import org.junit.jupiter.api.Test;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;

public class BlockingTest {

    @Test
    void 동기적_파일_읽기() {
        try {
            BufferedReader reader = new BufferedReader(new FileReader("test.txt"));
            String line;
            StringBuilder content = new StringBuilder();
            while ((line = reader.readLine()) != null) {
                content.append(line).append("\n");
            }
            reader.close();
            System.out.println(content.toString());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Test
    void 동기적_네트워크_요청() {
        try {
            URL url = new URL("http://example.com");
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");

            BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            String inputLine;
            StringBuilder response = new StringBuilder();
            while ((inputLine = in.readLine()) != null) {
                response.append(inputLine);
            }
            in.close();

            System.out.println("Response: " + response.toString());

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

 

 

 

Non-Blocking :

A 가 B 를 호출할 때 B 가 제어권을 바로 A 에게 넘겨 다른 일을 할 수 있도록 한다.
호출된 함수가 제어권을 넘겨 호출한 함수가 가지고 있다.
네트워크 요청, 비동기 I/O 작업

1. 호출과 반환
작업을 요청하면 바로 반환되고 작업이 완료될 때까지 대기하지 않고 다른 작업을 수행한다.

2. 상태 확인
작업의 완료 여부를 주기적으로 확인하거나 콜백 함수를 등록하여 작업이 완료되었을 때 이를 처리한다.

3. 비동기적 처리
작업이 완료되지 않은 상태에서도 다른 작업을 계속해서 처리할 수 있다.

 

import org.junit.jupiter.api.Test;

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.Future;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;

public class NonBlockingTest {

    @Test
    void 논블로킹_파일_읽기() {
        try {
            AsynchronousFileChannel channel 
            	= AsynchronousFileChannel.open(Paths.get("test.txt"), StandardOpenOption.READ);
            ByteBuffer buffer = ByteBuffer.allocate(1024);

            Future<Integer> operation = channel.read(buffer, 0);
            while (!operation.isDone()) {
                System.out.println("Reading file...");
            }

            buffer.flip();
            byte[] data = new byte[buffer.limit()];
            buffer.get(data);
            System.out.println(new String(data));
            buffer.clear();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Test
    void 논블로킹_네트워크_요청() {
        try {
            SocketChannel channel = SocketChannel.open();
            channel.configureBlocking(false); // Non-blocking 모드로 설정
            channel.connect(new InetSocketAddress("example.com", 80));

            while (!channel.finishConnect()) {
                System.out.println("Connecting...");
            }

            System.out.println("다른 작업 중");
            String request = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
            ByteBuffer buffer = ByteBuffer.wrap(request.getBytes());
            while (buffer.hasRemaining()) {
                channel.write(buffer);
            }

            ByteBuffer responseBuffer = ByteBuffer.allocate(1024);
            while (channel.read(responseBuffer) > 0) {
                responseBuffer.flip();
                byte[] data = new byte[responseBuffer.limit()];
                responseBuffer.get(data);
                System.out.println(new String(data));
                responseBuffer.clear();
            }

            channel.close();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

다른 관점에서 이해하기

멘토님과 대화를 나누며 배운 내용이다.

 

Sync / Async 는 어떤 스레드에서 처리할 것인가가 핵심이다.


구글링으로 알게된 Sync와 Async의 차이점은
"호출된 함수의 종료를 호출한 함수가 처리하는지 호출된 함수가 처리하는지에 대한 기준으로 나뉜다" 였다.

 

 

Sync는 호출된 함수가 종료될 때까지 호출한 함수가 기다린다.

즉 호출된 함수가 완료될 때까지 제어권을 반환하지 않으며 호출한 함수는 작업이 완료될 때까지 블로킹된다.

Async는 호출된 함수가 호출한 함수에게 즉시 제어권을 반환한다.

호출된 함수는 작업을 백그라운드에서 처리하고, 완료되면 콜백을 호출하거나 객체를 통해 결과를 전달한다.

 

 

생각해볼 방향은 "어떤 스레드가 작업을 처리할 것인가에 대한 기준"이다.

드라이브 스루를 이용하는 상황을 예시로 설명한다.

 

sync는 호출한 스레드가 직접 작업을 수행하고, 작업이 완료될 때까지 다른 작업을 하지 않는다.

즉 모든 작업이 같은 스레드에서 수행된다.

 

Async는 호출된 함수는 새로운 스레드, 이벤트 루프 또는 스레드 풀을 사용하여 작업을 수행할 수 있다.

호출한 스레드는 작업이 완료되기를 기다리지 않고 다른 작업을 수행할 수 있다.

즉, 호출한 함수가 동작하던 스레드는 호출된 함수와는 다른 스레드에서 동작하므로 호출한 함수가 동작하던 스레드 자체는 사용이 가능한 상태가 되는 것이다.

 

 

 


내가 이해한 Sync / Async.

 

운전자가 드라이브 스루로 본인이 마실 음료를 주문하고 직원이 제조에 들어갔다.
조수석에 있던 동승자가 본인이 마실 음료를 추가로 주문하려고 한다.


동기

직원이 한 명이라 제조중인 작업이 끝나야 주문을 받고 제조를 진행한다.

 

 

비동기

 

다른 직원을 불러 동승자의 음료를 제조하도록 시키고 제조하던 운전자의 음료를 마저 제조한다.

 

 

이 예시로 보자면 직원이 스레드, 차량은 작업을 의미한다.

즉, 본인의 스레드가 작업을 한다면 sync, 다른 스레드에 작업을 넘긴다면 async 가 된다.

 

 

 


Blocking / Non-Blocking 은 스레드 내에서 일어나는 일이 핵심이다.


구글링으로 알게된 Blocking과 Non-Blocking의 차이점은
"호출된 함수가 호출한 함수에게 제어권을 바로 주는지로 나뉜다." 였다.

 


Blocking
은 스레드 내에서 작업이 완료될 때까지 대기 상태에 머무르게하므로 해당 스레드가 다른 작업을 수행할 수 없다.

Non-Blocking은 스레드 내에서 작업이 완료되지 않아도 스레드가 멈춰있지 않고 다른 작업을 수행할 수 있다.

 

쉽게 말해 실행 중에 함수를 호출했을 때, 결과를 기다려야 하면 Blocking,

결과를 기다리지 않고 다른 작업을 실행할 수 있는 상태라면 Non-Blocking이라고 할 수 있다.

 

자바스크립트에서는 Blocking을 자바스크립트의 로직 이외에 Node.js 엔진이 기다려야 되는 상황을 말한다.

 

파일을 읽어올 때 I/O 작업은 두가지 방식으로 처리할 수 있다.

끝날 때까지 아무것도 하지 않고 기다리는 방법과, 호출해두고 다른 일을 하다가 호출했던 데이터가 필요할 때 호출해서 확인하는 방법이 있다.

 

스레드가 대기 상태에 머무른다는 의미는 스레드가 살아서 멈춰있다는 의미이다. 

즉 스레드가 아무것도 하지 않는 상태로 코어에 존재하며 자원을 소모하고 있다는 것이다. 언제 종료될 지 모르는 상태이므로 주기적으로 확인해야 한다. 

 

Non-Blocking은 불필요하게 소모되는 자원(CPU)을 다른 스레드에 양보한다. 

쉽게 말해 파일을 읽는 경우 I/O 작업은 커널이 처리하므로 커널에게 I/O 요청을 전달하고 다른 작업을 수행한다.

I/O 작업이 끝나면 커널이 프로그램에게 알려준다.(콜백, 이벤트 등)

 

Non-Blocking I/O에서는 커널이 I/O 작업을 처리하고 완료되면 프로그램에 알려주는 방식으로 동작하는데 이 과정에서 스레드는 스케줄링에 걸리지 않고 다른 작업을 수행할 수 있다.

 

커널이 작업을 마치고 인터럽트를 처리하면(ex: 이벤트 루프) 스레드는 다시 스케줄링에 포함되어 CPU를 할당받아 작업을 진행한다.

 

 

 

 

내가 이해한 Blocking / Non-Blocking.

스레드가 CPU에서 놀고있다면 Blocking, CPU를 비워둔다면 Non-Blocking 이 된다.

 

두 대의 차량이 드라이브 스루로 진입하여 음료를 주문하려고 한다.
첫 번째 차량이 진입하여 음료를 주문하고 직원이 음료를 제조하기 시작한다.


블로킹

첫 번째 차량은 음료를 받을 때까지 자리를 지키며 기다린다.

음료를 받은 첫 번째 차량이 드라이브 스루를 빠져나가고 두 번째 차량이 음료를 주문한다.

 

 

논블로킹

 

음료를 주문한 첫 번째 차량은 주차장에 주차한다.

두 번째 차량이 음료를 주문하고 주차장에 주차한다.

첫 번째 차량이 주문한 음료가 완성되면 직원은 운전자에게 알린다.

첫 번째 차량의 운전자는 드라이브 스루로 재진입하여 제조가 끝난 음료를 가져간다.

두 번째 차량이 주문한 음료가 완성되면 직원은 운전자에게 알린다.

두 번째 차량의 운전자는 드라이브 스루로 재진입하여 제조가 끝난 음료를 가져간다.

 

이 예시로 이해하자면 드라이브스루/직원은 CPU를 의미하고 운전자는 스레드를 의미한다.

 

 

 

 

그렇다면 조합을 살펴보자.

회사에서 업무를 처리하는 과정을 예시로 설명한다.

 

내가 이해한 Sync/Async + Blocking/Non-Blocking 조합.

 

1인 사무실에 사장님이 출근하여 첫 번째 업무를 처리하기 시작했다.
첫 번째 업무를 처리하던 도중 두 번째 업무의 결과가 필요하다.

 

Sync + Blocking

현재 Java에서 작성하는 대부분의 코드의 형식이고 API의 기본적인 CRUD가 여기에 해당한다.

동기 블로킹

 

첫 번째 업무를 잠시 미뤄두고 두 번째 업무를 처리하기 시작한다.

두 번째 업무를 처리하고 얻은 결과를 첫 번째 업무에 적용시키고 남은 첫 번째 업무를 처리한다.

 

 

 

Async + Blocking

비동기 블로킹

사무실을 확장하여 직원을 고용했다.

두 번째 업무를 직원에게 두 번째 업무를 처리하도록 지시하고 끝날 때까지 사무실에서 휴식을 취한다.

두 번째 업무를 끝낸 직원이 사장님에게 보고서를 제출한다.

사장님은 결과를 업무에 적용시켜 남은 첫 번째 업무를 처리한다.

 

 

 

Sync + Non-Blocking

동기 논블로킹

첫 번째 업무를 보류하고 사무실을 비우고 외근을 나간다.

외근 중 두 번째 업무를 처리한다.

두 번째 업무를 처리하고 얻은 결과를 가지고 회사로 돌아온다.

결과를 첫 번째 업무에 적용시키고 남은 첫 번째 업무를 회사에서 처리한다.

사장님이 회사를 비운 사이 다른 직원이 출근하여 다른 업무를 처리할 수 있다.

 

 

 

 

Async + Non-Blocking

비동기 논블로킹

사무실을 확장하여 직원을 고용했다.

두 번째 업무를 직원에게 두 번째 업무를 처리하도록 지시하고 외근을 보낸다.

외근을 보내고 사장님은 첫 번째 업무를 마저 처리한다.

외근 중 두 번째 업무를 끝낸 직원이 회사로 돌아와 사장님에게 보고서를 제출한다.

사장님은 두 번째 업무의 결과를 첫 번째 업무에 첨가한다.

 

 

문제.

멘토님께서 진짜 비동기로 처리하면 동기보다 좋은지에 대해 물으셨다.

비동기로 처리하면 처리하는 과정중에 화면에 처리중이라는 정보를 출력할 수 있어 좋을 것 같다고 생각했다.

 

request가 들어오면 response를 내보내는 API 서버에서 중간중간 response를 보내주는 것은 할 수는 있지만 안한다.

스프링 MVC는 스레드풀 모델을 사용하고 있다. 


스레드풀


여러 스레드를 미리 생성하여 작업 큐에 있는 작업을 효율적으로 처리하는 기술
미리 정해진 수의 스레드를 생성한다.
실행할 작업을 작업 큐에 보관한다.
작업 큐에서 스레드 풀 내의 스레드가 가져와 실행하고, 작업이 완료되면 다음 작업을 위해 대기한다.
스레드는 작업이 종료되면 종료되지 않고 다른 작업을 처리하기 위해 재사용된다.

 

 

즉 스레드 풀은 request가 들어와 response를 만들때까지 하나의 스레드는 하나의 request만을 담당한다.

다른 request가 들어오면 다른 스레드에 할당되므로 Async로 로직을 작성하고 콜백을 기다리고 있어도 다른 일을 처리할 수 없다. 

어찌저찌 중간중간 response를 넘겨줄 수는 있지만 오히려 성능면에서는 장점이 없게 된다.

어싱크 논블로킹이 좋다고 하지만 아직 MVC 모델이 많이 사용되는 이유라고도 하셨다.

 

 

이제 조금 더 이해한 내용을 바탕으로 CompletableFuture에 대해 다시 알아봐야겠다.
기존에 작업한 코드와 CompletableFuture로 작업한 코드의 차이점도 짚고 넘어가야겠다.

반응형