java NIO!

블로킹/논블로킹, 동기/비동기

출처: https://homoefficio.github.io/2017/02/19/Blocking-NonBlocking-Synchronous-Asynchronous

블로킹/논블로킹: 제어권(함수실행권)을 언제 넘겨주느냐의 차이 동기/비동기: 함수가 작업을 완료했는지를 누가 체크하느냐의 차이

javaio5

4가지 모두 개념적인 요소이기에 조합하여 사용하고, 코드에서 마주치는 상황은 2개 개념이 조합된 경우가 많다.

주황이 어플리케이션 스레드, 초록이 file i/o 를 수행하는 커널 스레드라 생각하면 된다.

  • Sync/Blocking
    • 일반적인 file i/o 를 호출하고 읽어올때 까지 대기하는 경우
    • file.read() 가 대표적
  • Async/Non-Blocking
    • 흔히 비동기 프로그래밍이 부르는 함수들, 코루틴 등
    • asyncFileChannel.read() 가 대표적

Async/BlockingSync/Non-Blocking 는 언어차원에서 지원하는 함수는 없고 프로그래밍적으로 처리하는 내용이다.

이 예에선 주황, 초록 모두 어플리케이션 스레드라 가정

  • Sync/Non-Blocking
    • 초록 스레드를 생성하고 전처리를 수행하고 공유자원의 상태에 따라 후처리를 마저 진행하는 경우
    • 콜백으로 처리하는것이 훨씬 효율적이라 잘 사용하지 않음.
  • Async/Blocking
    • Async/Non-Blocking 시스템에서 실수로 Sync/Blocking 함수를 호출했을 때,
    • 장애 발생 요소, 안티패턴이다.

고전적인 비동기 프로그래밍 방식으로 Sync/Blocking 을 수행하는 스레드를 여러개 만들어 동시처리를 수행해 왔다.
하지만 스레드가 늘어나면 그만은 컨텍스트 스위칭이 일어나게 되고 임계영역이나 공유데이터에 대한 동시성 접근도 제어해야 한다.

때문에 하나의 스레드가 동시에 여러개의 작업을 수행하는 Async/Non-Blocking 방법을 사용한다.
I/O 처리를 수행해야할 때 스레드가 block & wakeup 되지않고 바로 다른작업을 수행하는 방식이다.

Java NIO

IO는 스트림(Stream)이라는 단방향 통로를 생성해서 외부 데이터와 통신, 연결 클라이언트 수가 적고 대용량, 순차처리에 적합

NIO는 채널(Channel)이라는 양방향 통로를 생성해서 외부 데이터와 통신, 연결 클라이언트 수가 많고 소규모 빠른 대응에 적합

자바 NIO (New IO) 는 기존의 자바 IO API 를 대체하기 위해 java 1.4 부터 도입

자바 NIO 는 다음과 같은 핵심 컴포넌트로 구성되어있다.

  • Selectors
  • Channels
  • Buffers

javaio1

https://www.slideshare.net/kslisenko/networking-in-java-with-nio-and-netty-76583794

Non Blokcing and Multiplexing IO 모델

기존의 Blocking 모델의 경우 [listen(), connect(), accept(), recv(), send(), read(), write(), recvfrom(), sento(), close()] 등의 함수는 커널 I/O 버퍼 -> 유저 I/O 버퍼로 복사 후 이용해야 하기에 복사완료될때 까지 스레드가 봉쇄, 시스템 콜이 발생할 수 있다.

javaio5

process block : 애플리케이션에서 I/O 작업을 하는 경우 시스템콜이 발생하며 스레드는 데이터 준비가 완료될 때까지 대기합니다. 유저 프로세스는 유저 I/O 버퍼 에만 접근이 가능하기 때문입니다.

입출력 데이터가 준비될때까지 무한정 block되어 여러 클라이언트의 입출력을 동시에 처리하려면 스레드를 여러개 만들어야 한다.

반대로 Non-Blocking은 I/O작업을 진행하는 동안 유저 프로세스의 작업을 중단시키지 않는다.

javaio6

어플리케이션은 반복문을 돌면서 지속적으로 recvfrom 함수를 호출해 커널 I/O 버퍼에 데이터가 준비되었는지 묻는다.

데이터가 준비 되었다면 유저 I/O 버퍼에 데이터를 복사해주고 성공 반환값을 전달하고

데이터가 준비되지 않았다면 준비중을 뜻하는 EWOULDBLOCK 을 반환한다.

이렇게 되면 실제 수신되는 데이터가 없음에도 무한루프를 돌며 버퍼에 데이터 여부를 계속 확인하게 되는데

이를 방지하기 위해 I/O Multiplexing 을 사용한다.

커널에서는 하나의 스레드가 여러 개의 소켓을 핸들링 할 수 있는 [select, poll, epoll, kqueue, IOCP] 같은 시스템 콜을 제공하고 있다.

epooll 은 리눅스, kqueue 는 맥OS, IOCP 는 솔라리스와 같은 OS 에서 실행된다
select 와 동일한 기능이지만 성능때문에 각 OS에 전문화된 함수를 사용한다.

먼저 I/O MultiplexingBlocking 방식을 그림으로 설명한다.

javaio7

select 함수는 등록된 I/O 작업들중 어느 하나라도 data read 가 완료되면 block 되었던 스레드를 깨우도록 하는 기능이다.
select 함수를 호출해, 여러개의 소켓들 중 recvfrom 이 가능한 소켓이 생길 때까지 block 시킨다.

select 의 결과로 readable 가능한 recvfrom을 호출할 수 있는 소켓의 목록이 반환되면, 해당 소켓들에 대해 recvfrom을 호출한다.

읽어들여온 각종 데이터들을 처리할 작업들을 스레드풀의 스레드들에게 분배한다.

select 함수가 block 이 발생시키긴 하지만 각 커널 I/O 버퍼 를 돌면서 잡업완료가된 버퍼를 찾아 굉장히 빠른시간안에 block 이 해제한다.
cpu 점유율을 높혀 다른 스레드의 스케줄링을 위협하진 않는다.

Selectors

javaio4

자바 NIO 에는 위에서 설명한 select 함수를 사용하는 selectors 의 개념을 포함하고 있다.

selector 는 여러개의 채널에서 발생하는 이벤트(연결이 생성됨, 데이터가 도착함 등) 를 모니터링할 수 있는 객체다.
하나의 selector(스레드)에서 여러 채널에 대해 지속적으로 모니터링 한다.

selector에 하나 이상의 채널을 등록한 후에는 select() 메소드를 호출할 수 있다.
select() 메소드는 accept, connect, read, write 이벤트에 대해 준비(ready) 되어 있는 채널을 반환한다.

그외 selector 객체가 제공하는 필수 함수는 아래와 같다.

  • select() - 등록한 이벤트에 대해 하나 이상의 채널이 준비 될 때까지 봉쇄(block) 하고 몇개의 채널이 준비되었는지 준비된 채널의 수를 반환한다. (마지막 select()를 호출한 이후 준비된 채널 수)
  • select(long timeout) - 최대 timeout(ms) 동안만 봉쇄한다는 점을 제외하면 select()와 동일.
  • selectNow() - select와 달리 봉쇄하지 않음. 준비된 채널이 있으면 즉시 반환.
  • selectedKeys() - select() 메서드를 통해 하나 이상의 준비된 채널이 발생하면, selectedKeys() 메서드를 사용해 준비된 채널의 집합을 반환 받음.

일반적으로 아래와 같이 event 를 감지하여 NIO 처리를 진행한다.

while (selector.select() > 0) {
    Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
    while (keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();
        //키 셋에서 제거.
        keyIterator.remove();
        if (!key.isValid()) continue;
        if (key.isAcceptable()) accept(selector, key); // connection was accepted by a ServerSocketChannel
        else if (key.isConnectable()) System.out.println(""); // connection was established with a remote server
        else if (key.isReadable()) receive(selector, key); // channel is ready for reading
        else if (key.isWritable()) send(selector, key); // channel is ready for writing
    }
}

SelectionKey

필드로 가지고 있는 구성요소에 대해 알아보자.

The interest set - 채널이 확인하고자 하는 이벤트 집합

이벤트 종류

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE
int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;

The ready set - 채널에서 준비되어 처리(handle) 가능한 이벤트의 집합

int readySet = SelectionKey.readyOps();

// 혹은
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

The Channel, The Selector

SelectionKeyChannel, Selector 에 접근 가능

Channel  channel  = selectionKey.channel();
Selector selector = selectionKey.selector();

An attached object (optional)

SelectionKey에 객체를 첨부(attach) 가 정보나 채널에서 사용하는 버퍼와 같은 객체들을 쉽게 첨부할 수 있음.

// 3번째 매개변수로 넣거나 아래 attach 메서드 사용
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject); 
// selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();

생성된 채널을 selector 에 등록하기 위해 channel.register 메서드 호출

Channels

자바 NIO 의 모든 IO는 Channel 로 시작,

기존 자바 IOstream으로 통신함

  1. 채널을 통해서는 양방향 (읽고 쓰기) 가능, 스트림은 단방향.
  2. 채널은 비동기적(asynchronously)으로 읽고 쓰기 가능.
  3. 채널은 항상 버퍼에서 부터 읽거나 버퍼로 씁니다.

Channels 종류

  1. FileChannel - 파일에 데이터를 읽고 쓴다.
  2. DatagramChannel - UDP를 이용해 네트워크를 통해 데이터를 읽고 쓴다.
  3. SocketChannel - TCP를 이용해 네트워크를 통해 데이터를 읽고 쓴다.
  4. ServerSocketChanel - 들어오는 TCP 연결을 수신(listening)할 수 있다. 들어오는 연결마다 SocketChannel이 만들어진다.

Buffers

자바 NIO에 기본적으로 구현되어 있는 버퍼 목록.

  • Buffers
  • ByteBuffer
  • MappedByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer
  1. 버퍼에 데이터 쓰기 - 쓰기모드
  2. buffer.flip() 호출 - 읽기모드로 전환
  3. 버퍼에서 데이터 읽기 - 읽기모드
  4. buffer.clear() 혹은 buffer.compact() 호출

(clear() 메서드는 버퍼 전체를 지우고, compact() 메서드는 이미 읽은 데이터만 지웁니다.)

출처

https://jongmin92.github.io/2019/02/28/Java/java-with-non-blocking-io/ https://12bme.tistory.com/231

카테고리:

업데이트: