java NIO!
블로킹/논블로킹, 동기/비동기
출처: https://homoefficio.github.io/2017/02/19/Blocking-NonBlocking-Synchronous-Asynchronous
블로킹/논블로킹: 제어권(함수실행권)을 언제 넘겨주느냐의 차이 동기/비동기: 함수가 작업을 완료했는지를 누가 체크하느냐의 차이
4가지 모두 개념적인 요소이기에 조합하여 사용하고, 코드에서 마주치는 상황은 2개 개념이 조합된 경우가 많다.
주황이 어플리케이션 스레드, 초록이 file i/o 를 수행하는 커널 스레드라 생각하면 된다.
Sync/Blocking
- 일반적인
file i/o
를 호출하고 읽어올때 까지 대기하는 경우 file.read()
가 대표적
- 일반적인
Async/Non-Blocking
- 흔히 비동기 프로그래밍이 부르는 함수들, 코루틴 등
asyncFileChannel.read()
가 대표적
Async/Blocking
과 Sync/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
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 버퍼
로 복사 후 이용해야 하기에 복사완료될때 까지 스레드가 봉쇄, 시스템 콜이 발생할 수 있다.
process block : 애플리케이션에서 I/O 작업을 하는 경우 시스템콜이 발생하며 스레드는 데이터 준비가 완료될 때까지 대기합니다. 유저 프로세스는
유저 I/O 버퍼
에만 접근이 가능하기 때문입니다.
입출력 데이터가 준비될때까지 무한정 block
되어 여러 클라이언트의 입출력을 동시에 처리하려면 스레드를 여러개 만들어야 한다.
반대로 Non-Blocking
은 I/O작업을 진행하는 동안 유저 프로세스의 작업을 중단시키지 않는다.
어플리케이션은 반복문을 돌면서 지속적으로 recvfrom
함수를 호출해 커널 I/O 버퍼
에 데이터가 준비되었는지 묻는다.
데이터가 준비 되었다면 유저 I/O 버퍼
에 데이터를 복사해주고 성공 반환값을 전달하고
데이터가 준비되지 않았다면 준비중을 뜻하는 EWOULDBLOCK
을 반환한다.
이렇게 되면 실제 수신되는 데이터가 없음에도 무한루프를 돌며 버퍼에 데이터 여부를 계속 확인하게 되는데
이를 방지하기 위해 I/O Multiplexing
을 사용한다.
커널에서는 하나의 스레드가 여러 개의 소켓을 핸들링 할 수 있는 [select, poll, epoll, kqueue, IOCP]
같은 시스템 콜을 제공하고 있다.
epooll 은 리눅스, kqueue 는 맥OS, IOCP 는 솔라리스와 같은 OS 에서 실행된다
select 와 동일한 기능이지만 성능때문에 각 OS에 전문화된 함수를 사용한다.
먼저 I/O Multiplexing
의 Blocking
방식을 그림으로 설명한다.
select
함수는 등록된 I/O
작업들중 어느 하나라도 data read
가 완료되면 block
되었던 스레드를 깨우도록 하는 기능이다.
select
함수를 호출해, 여러개의 소켓들 중 recvfrom
이 가능한 소켓이 생길 때까지 block
시킨다.
select
의 결과로 readable 가능한 recvfrom
을 호출할 수 있는 소켓의 목록이 반환되면, 해당 소켓들에 대해 recvfrom
을 호출한다.
읽어들여온 각종 데이터들을 처리할 작업들을 스레드풀의 스레드들에게 분배한다.
select
함수가 block
이 발생시키긴 하지만 각 커널 I/O 버퍼
를 돌면서 잡업완료가된 버퍼를 찾아 굉장히 빠른시간안에 block
이 해제한다.
cpu
점유율을 높혀 다른 스레드의 스케줄링을 위협하진 않는다.
Selectors
자바 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
SelectionKey
로 Channel, 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
로 시작,
기존
자바 IO
는stream
으로 통신함
- 채널을 통해서는 양방향 (읽고 쓰기) 가능, 스트림은 단방향.
- 채널은 비동기적(
asynchronously
)으로 읽고 쓰기 가능. - 채널은 항상 버퍼에서 부터 읽거나 버퍼로 씁니다.
Channels 종류
FileChannel
- 파일에 데이터를 읽고 쓴다.DatagramChannel
- UDP를 이용해 네트워크를 통해 데이터를 읽고 쓴다.SocketChannel
- TCP를 이용해 네트워크를 통해 데이터를 읽고 쓴다.ServerSocketChanel
- 들어오는 TCP 연결을 수신(listening)할 수 있다. 들어오는 연결마다 SocketChannel이 만들어진다.
Buffers
자바 NIO에 기본적으로 구현되어 있는 버퍼 목록.
Buffers
ByteBuffer
MappedByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
- 버퍼에 데이터 쓰기 - 쓰기모드
buffer.flip()
호출 - 읽기모드로 전환- 버퍼에서 데이터 읽기 - 읽기모드
buffer.clear()
혹은buffer.compact()
호출
(
clear()
메서드는 버퍼 전체를 지우고,compact()
메서드는 이미 읽은 데이터만 지웁니다.)
출처
https://jongmin92.github.io/2019/02/28/Java/java-with-non-blocking-io/ https://12bme.tistory.com/231