본문 바로가기
Java

쓰레드와 프로세스, 그리고 자바 비동기 API

by Jordy-torvalds 2020. 3. 10.
반응형

Thread And Process in OS Level

프로세스란?

  • 독립된 실행의 단위
  • CPU 자원은 OS의 스케줄러에 의해 Time Slice 만큼 실행.
  • Memory는 OS로 부터 할당 받은 메모리 공간 사용.(CODE, DATA, HEAP, STACK)
  • 쓰레드(s) + 메모리공간
  • 프로그램
    • 코드: 개발자가 작성한 코드
    • 데이터: 컴파일 타임에 정해 지는 전역 변수 및 상수 데이터 영역 확보
    • 런타임에 힙과 스택 영역 생성

CPU - Memory 통신 방법

  • CPU와 메모리에 직접 접근 할 수 없어서 가상 메모리를 두는데, 가상 메모리는 페이지 테이블이라고도 불리며 프로세스의 페이지 정보를 저장한다. 개별 프로세스의 페이지는 다른 프로세스가 접근할 수 없다.

가상 메모리 또는 가상 기억 장치는 RAM을 관리하는 방법의 하나로, 각 프로그램에 실제 메모리 주소가 아닌 가상의 메모리 주소를 주는 방식을 말한다.

페이징이란 보통 메모리 스페이스를 잘게 나눠서 메모리에 저장하는 가상화 방식중 하나이다.

컴퓨터 과학에서 페이지 테이블(page table)은 페이징 기법에서 사용되는 자료구조로서, 프로세스의 페이지 정보를 저장하고 있는 테이블이다. 하나의 프로세스는 하나의 페이지 테이블을 가진다. 테이블은 페이지 번호인 색인과 페이지에 할당된 메모리의 시작 주소인 내용으로 구성되어 있다.

프로세스와 쓰레드

프로세스의 메모리는 위와 같이 구성이 된다. P1의 프로세스는 P2의 메모리를 침범 할 수 없는데, 가상 메모리는 프로세스간 메모리 공간을 논리적으로 차단하기 때문이다.

그래서 앞서 말했 듯이 프로세스간 메모리를 공유할 수 없다고 한 것이다.

P2 프로세스 내부에는 2개의 쓰레드 (T1, T2)가 있다. 두 쓰레드는 개별 쓰레드가 사용할 고유한 스택 영역과 공유할 수 있는 힙, 데이터, 코드 영역을 가진다.

프로세스 내 쓰레드를 만드는 것 또한 비용이 들지만, 프로세스를 만드는 것 보다는 비용이 저렴하고 멀리 프로세스에게 없는 공유 메모리 공간(힙) 이 있어 성능이 뛰어나고 코드로 제어하는 것 또한 비교적 용이하다. 이를 멀티 쓰레드 라고 부른다.

컨텍스트 스위치

스케줄러에 의해 쓰레드는 타임 슬라이스만큼 CPU를 사용한 후 작업을 멈추고 CPU를 다음 스레드가 점유하는 것.

  • 저장: 반납되는 쓰레드는 현재 상태를 저장.
  • 복원: 실행되는 쓰레드는 메모리에서 저장된 상태를 복구.
  • 큐에 저장된 쓰레드를 스케줄링에 따라 순차적으로 처리하며, 그 과정에서 저장/복원이 반복되며 오버헤드 발생.

ThreadPool

  • 스레드를 미리 만들어 놓고 재사용하는 것.
  • 비용 감소: 쓰레드 생성 비용 - cpu, memory
  • 쓰레드 풀은 쓰레드 풀 매니저에 의해 관리되며, 쓰레드 큐에 쌓인 쓰레드를 적절히 처리.

Thread And Process in JVM

JVM Thread Cost

  • Thread 생성 비용(OS + JVM)
  • Context Switching
  • Garbage Collection

굵게 표시한 항목은 OS 레벨 쓰레드의 비용에서 더 추가적으로 비용이 들어가는 항목.

JAVA Async API

비동기가 필요한 상황

  • CPU Expensive: CPU 자원을 많이 사용하는 코드 중 분할 가능한 코드를 병렬화 하여 시간 단축
  • IO Blocking: HDD, NIC 등 하드웨어 자원의 IO는 느린편. IO 작업을 요청한 쓰레드는 IO 작업 완료를 대기하며 이는 스레드 자원 낭비로 이어짐.

대표적인 비동기 API, Thread

초기에는 Thread와 흐름 제어를 위해 Object 내 wait(), notify() 사용

Future API, 비동기 작업 결과 반환

  • interface Future
    • V get(): 값을 가져오기 위해 wait, notify를 하나의 메서드로 추상화.
    • boolean isDone();
  • class FutureTask
    • Future 인터페이스의 구현체
  • interface callable
    • V call()
    • 기존의 Thread에 사용되는 Runnable과 달리 리턴 타입이 있음

예시

public class FuturePrac {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable callable = FuturePrac::getApi;
        ExecutorService es = Executors.newSingleThreadExecutor();
        Future future = es.submit(callable);
        System.out.println("Future 할당 후!");
        String result = future.get(); // getApi가 완료되어야 반환

        System.out.println(result);
        es.shutdown();
    }

    private static String getApi() {
        System.out.println("getApi()");
        return "result";
    }
}

쓰레드 풀

풀 내에 모든 쓰레드가 사용 중일 경우 다른 쓰레드가 완료되어야 처리가 가능하므로, 지연이 발생할 수 있음. new CacheThreadPool 은 요청에 따라 새로운 쓰레드를 생성해내는데 한 클라이언트의 요청에 7개의 쓰레드를 생성할 경우 300명의 고객이 요청하게 되면 2100개의 쓰레드가 생성됨. 결과적으로 서비스 응답 지연 및 장애로 이어짐.

결론적으로 쓰레드 풀의 쓰레드를 어느정도 적당선에서 고정해서 쓰는 것을 권장.

CompletableFuture API, 의존성 있는 비동기 작업 처리

순수 Future API로 구현할 시 콜백 지옥에 빠질 수 있음.

CompletableFuture class는 interface인 Future, CompletionStage 의 구현체

예시 1

public class CompletableFuturePrac {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture cf = new CompletableFuture();
        new Thread(()->cf.complete("hello")).start();
        String result = cf.get();
        System.out.println(result);
    }
}

예시 2. 메서드 체이닝을 통한 우아한 콜백 처리

public class CompletableFuturePrac {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        String result = (String) CompletableFuture
                        .completedFuture("Hello")
                        .thenApply(msg->getFirstApi(msg)) // 람다
                        .thenApply(CompletableFuturePrac::getSecondApi) // 메서드 레퍼런스
                        .thenApplyAsync("I`m going home") // 비동기 처리를 위한 쓰레드 생성
                        .get();
        System.out.println(result);
    }

    private static String getFirstApi(String msg) {
        return msg + ", jordy!\n";
    }

    private static String getSecondApi(String msg) {
        return msg +"Where r u going?";
    }
}

예시 3. CompletedFuture * 3 조합

public class CombineCompletableFuturePrac {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture cf1 = CompletableFuture.completedFuture("안녕하세요!");
        CompletableFuture cf2 = CompletableFuture.completedFuture("죠르디 입니다!");
        CompletableFuture cf3 = CompletableFuture.completedFuture("반가워요!");

        String msg = cf1.thenCombine(cf2, (x,y) -> x +"\n"+y)
                .thenCombine(cf3, (x,y) -> x+"\n"+y)
                .get();
        System.out.println(msg);
    }
}

NIO(New IO)

  • NIO
    • JDK 1.4 NIO
    • JDK 1.7 NIO2: 이전 버전에 비해 비동기 채널 등의 네트워크 지원을 대폭 강화
      • java.nio의 하위 패키지(java.nio.channels, java.nio.charset, java.nio.file)에 통합.
  • NEW IO 구성
    • Native IO
    • Non-Blocking IO
  • 패키지 별 주요 내용
    • java.nio: 다양한 버퍼 클래스
    • java.nio.channels: 파일 채널, TCP 채널, UDP 채널 등의 클래스
    • java.nio.channels.spi: java.nio.channels 패키지를 위한 서비스 제공자 클래스
    • java.nio.charset: 문자셋, 인코더, 디코더 API
    • java.nio.charset.spi: java.nio.charset 패키지를 위한 서비스 제공자 클래스
    • java.nio.file: 파일 및 파일 시스템에 접근하기 위한 클래스
    • java.nio.file.attribute: 파일 및 파일 시스템의 속성에 접근하기 위한 클래스
    • java.nio.file.spi: java.nio.file 패키지를 위한 서비스 제공자 클래스

일반적으로 특정 데이터를 사용하기 위해서는, JVM의 메모리 공간 중 힙에 카피하여 사용해야하는 반면 Native IO는 원래 있는 데이터에 직접 접근 후 핸들리하는 방식으로 성능적으로 훨씬 뛰어남.

Non-blocking I/O란, 입출력 처리는 시작만 해둔 채 완료되지 않은 상태에서 다른 처리 작업을 계속 진행할 수 있도록 멈추지 않고 입출력 처리를 기다리는 방법을 말한다.

세부 내용: https://palpit.tistory.com/644

BackPressure

들어온 요청을 처리하는 것이 불가능한 것으로 판단될 경우 거부하는 기능.

결론

비동기, NIO 모두 특정 이벤트가 발생했을 때 처리 후 결과를 반화하는 방식으로, 둘 모두 이벤트 기반 프로그래밍이라고 볼 수 있음.

이벤트 기반 프로그래밍과 관련된 라이브러리는 정말 많으므로 선택과 집중이 필요

  • 리액티브-스트림즈
  • 리액터
  • 스프링 5 MVC (+ WebFlux)
반응형