1. 소개
Java BIO, NIO, AIO의 차이점과 원리에 대한 글은 많지만 주로 BIO와 NIO 사이에서 논의되는 반면, AIO에 대한 글은 거의 없고 대부분은 소개에 불과합니다. 개념 및 코드 예제.
AIO에 대해 학습할 때 다음과 같은 현상이 발견되었습니다.
1. Java 7이 2011년에 출시되어 비동기 IO라는 AIO라는 프로그래밍 모델이 추가되었지만 거의 12년이 지났고 네트워크 프레임워크인 Netty, Mina, Web 컨테이너와 같은 일반적인 개발 프레임워크 미들웨어는 여전히 NIO가 지배하고 있습니다. 톰캣, 언더토우.
2. Java AIO는 NIO 2.0이라고도 하는데 이것도 NIO 기반인가요?
3. Netty는 AIO 지원을 중단했습니다. https://github.com/netty/netty/issues/2515
4. AIO는 문제만 풀고 외로움을 풀어놓은 것 같다.
이러한 현상은 많은 사람들을 혼란스럽게 할 수밖에 없기 때문에 이 글을 쓰게 된 계기는 단순히 AIO의 개념을 되풀이하는 것이 아니라 그 현상을 통해 Java AIO의 본질을 어떻게 분석하고 생각하고 이해할 것인가 하는 것이었습니다.
2. 비동기란 무엇인가
2.1 우리가 알고 있는 비동기성
AIO의 A는 Asynchronous를 의미하는데, AIO의 원리를 이해하기 전에 "비동기"란 어떤 개념인지 알아보자.
비동기 프로그래밍에 대해 말하자면, 다음 코드 예제와 같이 일반 개발에서는 여전히 비교적 일반적입니다.
@Async
public void create() {
//TODO
}
public void build() {
executor.execute(() -> build());
}
@Async로 주석을 달든 스레드 풀에 작업을 제출하든 모두 동일한 결과로 끝납니다. 즉, 실행할 작업을 실행을 위해 다른 스레드로 넘기는 것입니다.
이때 소위 "비동기"는 멀티 스레드로 작업을 수행한다고 대략적으로 생각할 수 있습니다.
2.2 Java BIO 및 NIO는 동기식입니까 아니면 비동기식입니까?
Java BIO와 NIO가 동기식이든 비동기식이든 우리는 먼저 비동기의 개념에 따라 비동기 프로그래밍을 합니다.
2.2.1 바이오 예시
byte [] data = new byte[1024];
InputStream in = socket.getInputStream();
in.read(data);
// 接收到数据,异步处理
executor.execute(() -> handle(data));
public void handle(byte [] data) {
// TODO
}
BIO read() 시 스레드가 차단되더라도 데이터 수신 시 스레드를 비동기적으로 시작하여 처리할 수 있습니다.
2.2.2 NIO 예제
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
executor.execute(() -> {
try {
channel.read(byteBuffer);
handle(byteBuffer);
} catch (Exception e) {
}
});
}
}
public static void handle(ByteBuffer buffer) {
// TODO
}
같은 방식으로 NIO read()는 non-blocking이지만 select()를 통해 데이터 대기를 차단할 수 있으며 읽을 데이터가 있으면 스레드를 비동기적으로 시작하여 데이터를 읽고 처리합니다.
2.2.3 이해의 편차
이때 Java의 BIO와 NIO가 비동기식인지 동기식인지는 기분에 따라 다르다고 맹세합니다.
하지만 이런 경우 블로그 글을 많이 읽어보면 기본적으로 BIO와 NIO가 동기화되어 있다는 것이 명확해진다.
그렇다면 문제는 어디에 있으며 우리의 이해에서 편차를 일으킨 원인은 무엇입니까?
그것이 기준틀의 문제입니다.이전에 물리학을 공부할 때 버스에 탄 승객이 움직이고 있는지 정지해 있는지 기준틀이 필요합니다. 참조, 그는 고정되어 있습니다.
Java IO도 마찬가지입니다. 동기식인지 비동기식인지 정의하려면 참조 시스템이 필요합니다. IO가 어떤 모드인지 논의하고 있으므로 IO 읽기 및 쓰기 작업을 이해하고 다른 작업은 다른 작업을 시작합니다. 데이터를 처리하는 스레드는 이미 IO 읽기 및 쓰기 범위를 벗어났으므로 관여해서는 안 됩니다.
2.2.4 비동기 정의 시도
따라서 IO 읽기 및 쓰기 작업의 이벤트를 참조하여 IO 읽기 및 쓰기를 시작하는 스레드(읽기 및 쓰기를 호출하는 스레드)와 실제로 IO 읽기 및 쓰기를 수행하는 스레드를 정의하려고 합니다. 그들은 동일한 스레드이며 동기라고 부르고 그렇지 않으면 비동기라고 부릅니다 .
-
분명히 BIO는 동기식일 수 있습니다.in.read()를 호출하면 현재 스레드가 차단되고 데이터가 반환되면 원래 스레드가 데이터를 받습니다.
-
그리고 NIO는 동기화라고도 하며 그 이유는 동일합니다.channel.read()를 호출할 때 스레드가 차단되지는 않지만 데이터를 읽는 것은 여전히 현재 스레드입니다.
이 아이디어에 따르면 AIO는 IO 읽기 및 쓰기를 시작하는 스레드여야 하며 실제로 데이터를 받는 스레드는 동일한 스레드가 아닐 수 있습니다
.
2.3 Java AIO 프로그램 예제
2.3.1 AIO 서버 프로그램
public class AioServer {
public static void main(String[] args) throws IOException {
System.out.println(Thread.currentThread().getName() + " AioServer start");
AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open()
.bind(new InetSocketAddress("127.0.0.1", 8080));
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketChannel clientChannel, Void attachment) {
System.out.println(Thread.currentThread().getName() + " client is connected");
ByteBuffer buffer = ByteBuffer.allocate(1024);
clientChannel.read(buffer, buffer, new ClientHandler());
}
@Override
public void failed(Throwable exc, Void attachment) {
System.out.println("accept fail");
}
});
System.in.read();
}
}
public class ClientHandler implements CompletionHandler<Integer, ByteBuffer> {
@Override
public void completed(Integer result, ByteBuffer buffer) {
buffer.flip();
byte [] data = new byte[buffer.remaining()];
buffer.get(data);
System.out.println(Thread.currentThread().getName() + " received:" + new String(data, StandardCharsets.UTF_8));
}
@Override
public void failed(Throwable exc, ByteBuffer buffer) {
}
}
2.3.2 AIO 클라이언트 프로그램
public class AioClient {
public static void main(String[] args) throws Exception {
AsynchronousSocketChannel channel = AsynchronousSocketChannel.open();
channel.connect(new InetSocketAddress("127.0.0.1", 8080));
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("Java AIO".getBytes(StandardCharsets.UTF_8));
buffer.flip();
Thread.sleep(1000L);
channel.write(buffer);
}
}
2.3.3 비동기 정의 추측 결론
서버와 클라이언트 프로그램을 별도로 실행
서버를 실행한 결과,
주 스레드는 serverChannel.accept에 대한 호출을 시작하고 콜백을 모니터링하기 위해 CompletionHandler를 추가합니다.클라이언트가 연결되면 Thread-5 스레드는 완료된 수락 콜백 메서드를 실행합니다.
그 직후 Thread-5는 clientChannel.read 호출을 시작하고 콜백을 모니터링하기 위해 CompletionHandler를 추가했으며 데이터를 수신할 때 Thread-1은 완료된 읽기 콜백 메서드를 실행했습니다.
이 결론은 위의 비동기 추측과 일치합니다 .IO 작업(예: 수락, 읽기, 쓰기)을 시작하는 스레드는 작업을 최종적으로 완료하는 스레드와 동일하지 않습니다.이 IO 모드를 AIO라고 합니다 .
물론 이러한 방식으로 AIO를 정의하는 것은 우리의 이해를 위한 것일 뿐이며 실제로는 비동기 IO의 정의가 더 추상적일 수 있습니다.
3. AIO 예시는 생각하는 질문을 촉발합니다.
1. completed() 메서드를 실행하는 스레드는 누가 생성했으며 언제 생성되었습니까?
2. AIO 등록 이벤트 모니터링 및 실행 콜백은 어떻게 구현하나요?
3. 콜백 모니터링의 본질은 무엇입니까?
3.1 질문 1: completed() 메서드를 실행하는 스레드는 누가 생성했으며 언제 생성되었습니까?
일반적으로 이러한 문제는 프로그램 진입부터 파악해야 하지만 쓰레드와 관련된 문제이며, 실제로 쓰레드 스택의 실행 상태에서 쓰레드가 어떻게 동작하는지 알 수 있다.
AIO 서버 프로그램만 실행하고 클라이언트는 실행하지 않고 스레드 스택을 인쇄합니다(참고: 프로그램은 Linux 플랫폼에서 실행되며 다른 플랫폼은 약간 다릅니다)
스레드 스택을 분석하고 프로그램이 너무 많은 스레드를 시작하는지 확인합니다.
1. 스레드 Thread-0이 EPoll.wait() 메서드에서 차단됨
2. 스레드 스레드-1, 스레드-2. . . Thread-n(n은 CPU 코어 수와 같음)은 차단 대기열에서 작업을 가져오고 작업이 반환되기를 기다리는 블록을 차단합니다.
이쯤 되면 잠정적으로 다음과 같은 결론을 내릴 수 있다.
AIO 서버 프로그램이 시작된 후 이러한 스레드가 생성되고 스레드는 모두 차단 대기 상태가 됩니다.
그리고 이 쓰레드들의 실행이 Epoll과 관련이 있다는 것을 알게 되었는데, Epoll이라고 하면 Linux 플랫폼의 하단에 Epoll로 Java NIO가 구현되어 있다는 인상을 받았습니다. Java AIO도 Epoll로 구현되는 건가요 ? 이 결론을 확인하기 위해 다음 질문부터 논의한다.
3.2 질문 2: AIO 등록 이벤트 모니터링 및 실행 콜백 구현 방법
이 문제를 염두에 두고 소스 코드를 읽고 분석했을 때 소스 코드가 매우 길고 소스 코드 파싱은 지루한 과정으로 독자를 쉽게 멀어지게 할 수 있음을 발견했습니다.
긴 프로세스와 논리적으로 복잡한 코드를 이해하기 위해 여러 컨텍스트를 파악하고 핵심 프로세스를 찾을 수 있습니다.
등록 리스너를 clientChannel.read(…) 예제로 읽어보면 주요 핵심 프로세스는 다음과 같습니다.
1. 이벤트 등록 -> 2. 이벤트 듣기 -> 3. 이벤트 처리
3.2.1 1. 등록 이벤트
등록 이벤트는 EPoll.ctl(…) 함수를 호출하고 이 함수의 마지막 매개변수는 일회성인지 영구인지 지정하는 데 사용됩니다. 위의 코드 이벤트 | EPOLLONSHOT은 문자 그대로 일회성이라는 의미입니다.
3.2.2 2. 이벤트 모니터링
3.2.3 3. 이벤트 처리
3.2.4 핵심 프로세스 요약
위의 코드 흐름을 분석하면 각 IO 읽기 및 쓰기에 대해 경험해야 하는 세 가지 이벤트가 일회성, 즉 이벤트가 처리된 후 이 프로세스가 종료됨을 알 수 있습니다. IO 읽고 쓰려면 처음부터 다시 시작해야 합니다. 이런 식으로 프로그래밍의 복잡성을 크게 증가시키는 소위 죽음 콜백(콜백 메서드에 다음 콜백 메서드가 추가됨)이 있을 것입니다.
3.3 질문 3: 콜백 모니터링의 본질은 무엇입니까?
먼저 결론부터 말씀드리면 소위 모니터링 콜백의 본질은 커널 모드 함수(정확히 말하자면 read, write, epollWait와 같은 API)를 호출하는 사용자 모드 스레드입니다. 반환되지 않으면 사용자 스레드가 차단됩니다. 함수가 반환되면 차단된 스레드가 깨어나 소위 콜백 함수가 실행됩니다 .
이 결론을 이해하려면 먼저 몇 가지 개념을 소개해야 합니다.
3.3.1 시스템 호출 및 함수 호출
함수 호출:
함수를 찾고 함수에서 관련 명령을 실행합니다.
시스템 호출:
운영 체제는 소위 API라고 하는 사용자 응용 프로그램에 대한 프로그래밍 인터페이스를 제공합니다.
시스템 호출 실행 프로세스:
1. 시스템 호출 매개변수 전달
2. 일반적으로 시스템 호출은 코어 모드에서 실행되어야 하므로 트랩된 명령을 실행하고 사용자 모드에서 코어 모드로 전환합니다.
3. 시스템 콜 프로그램 실행
4. 사용자 상태로 돌아가기
3.3.2 사용자 모드와 커널 모드 간의 통신
사용자 모드 -> 시스템 호출을 통한 커널 모드.
커널 모드 -> 사용자 모드, 커널 모드는 사용자 모드 프로그램이 어떤 기능을 가지고 있는지, 매개 변수가 무엇인지, 주소가 어디에 있는지 알지 못합니다. 따라서 커널이 사용자 모드에서 함수를 호출하는 것은 불가능하고 신호를 보내야만 합니다.
커널 상태가 사용자 상태에서 능동적으로 함수를 호출하는 것은 불가능하므로 콜백이 있는 이유는 소위 콜백이 실제로는 자체 지시 및 자체 수행 사용자 상태라고 말할 수 있습니다. 모니터링 뿐만 아니라 콜백 기능도 실행합니다.
3.3.3 실제 사례를 통해 결론 확인
이 결론이 설득력이 있는지 검증하기 위해, 예를 들어 일반적으로 코드를 개발하고 작성하는 데 사용되는 IntelliJ IDEA는 마우스 및 키보드 이벤트를 수신하고 이벤트를 처리합니다.
규칙에 따라 먼저 스레드 스택을 인쇄하면 "AWT-XAWT" 스레드가 마우스 및 키보드와 같은 모니터링 이벤트를 담당하고 "AWT-EventQueue" 스레드가 이벤트 처리를 담당한다는 것을 알 수 있습니다.
특정 코드를 찾으면 "AWT-XAWT"가 while 루프를 수행하고 waitForEvents 함수를 호출하여 이벤트가 반환될 때까지 기다리는 것을 볼 수 있습니다. 이벤트가 없으면 스레드가 차단된 것입니다.
4. Java AIO의 본질은 무엇입니까?
1. 커널 모드는 사용자 모드 함수를 직접 호출할 수 없기 때문에 Java AIO의 본질은 사용자 모드에서만 비동기를 구현하는 것입니다. 이상적인 의미에서 비동기를 달성하지 못합니다.
이상적인 비동기
이상적인 의미에서 비동기란 무엇입니까? 다음은 온라인 쇼핑의 예입니다.
소비자 A와 택배 B의 두 가지 역할
-
A가 온라인 쇼핑을 할 때 결제할 자택 주소를 입력하고 주문을 제출하면 모니터링 이벤트 등록과 동일합니다.
-
상인은 상품을 배송하고 B는 상품을 A의 집으로 배송하는 콜백과 같습니다.
A는 온라인으로 주문한 후 후속 배송 프로세스에 대해 걱정할 필요가 없으며 계속해서 다른 일을 할 수 있습니다. B는 물건을 배달할 때 A가 집에 있든 없든 상관하지 않습니다 .
A의 쇼핑이 사용자 모드에서 이루어지고 B의 빠른 배송이 커널 모드에서 이루어진다고 가정하면 이러한 종류의 프로그램 작동 모드는 너무 이상적이며 실제로 실현할 수 없습니다.
현실의 비동기
A는 고급 주택가에 거주하고 있어 마음대로 들어갈 수 없고, 택배는 주택가의 대문까지만 배송이 가능하다.
A씨는 A씨가 출근하고 집에 없는 관계로 TV 등 비교적 무거운 물건을 구입해 친구 C에게 TV를 집으로 옮기는 일을 부탁했다.
A씨는 출근하기 전 문간에서 경비원 D씨에게 오늘 TV가 배달된다고 인사를 하고, TV가 동문까지 배달되면 C씨에게 전화를 걸어 와서 가지러 가달라고 부탁한다.
-
이때 A는 주문을 하고 D를 맞이하는데 이는 이벤트 등록과 같다. AIO에서는 EPoll.ctl(...) 등록 이벤트입니다.
-
문 앞에 쪼그려 앉아 있는 경비원은 이벤트를 듣는 것과 같습니다. AIO에서는 Thread-0 스레드입니다. Do EPoll.wait(…)
-
택배가 TV를 문 앞까지 배달했는데, 이는 IO 이벤트가 도착한 것과 같습니다.
-
경비원은 C에게 TV가 도착했다고 알리고 C가 와서 TV를 옮기는 것은 사건 처리와 같다.
AIO에서 Thread-0은 태스크를 태스크 큐에 제출합니다.
데이터를 가져오고 콜백 메서드를 실행하는 Thread-1 ~n.
이 과정에서 경비원 D는 항상 쪼그려 앉아 있어야 했고, 한 치도 움직일 수 없었습니다. 그렇지 않으면 TV가 문앞으로 배달될 때 도난 당할 것입니다.
친구 C도 A의 집에 묵어야 하는데, 누군가에게 일을 맡겼는데 물건이 왔을 때 그 사람이 없다는 건 좀 얄밉다.
따라서 실제 비동기와 이상적인 비동기는 서로 독립적이며 서로 간섭하지 않는데, 이 두 점은 서로 상반된다 . 경호원의 역할이 가장 크고, 지금이 그의 인생의 하이라이트다.
비동기 프로세스에서 이벤트 등록, 이벤트 수신, 이벤트 처리 및 멀티스레딩 활성화, 이러한 프로세스의 개시자는 모두 사용자 모드에서 처리되므로 Java AIO는 사용자 모드에서 비동기만 구현하며 이는 BIO로 먼저 차단됩니다. 및 NIO , wakeup 차단 후 비동기 스레드 처리를 시작하는 본질은 동일합니다.
2. Java AIO는 NIO와 동일하며 각 플랫폼의 기본 구현 방법도 다릅니다 EPoll은 Linux에서 사용되며 IOCP는 Windows에서 사용되며 KQueue는 Mac OS에서 사용됩니다. 원칙은 동일하며, 모두 IO 이벤트를 차단하고 대기하는 사용자 스레드와 큐에서 이벤트를 처리하는 스레드 풀이 필요합니다.
3. Netty가 AIO를 제거한 이유는 성능면에서 AIO가 NIO보다 높지 않기 때문입니다. Linux에도 기본 AIO 구현 세트(Windows의 IOCP와 유사)가 있지만 Java AIO는 Linux에서 사용되지 않고 EPoll로 구현됩니다.
4. Java AIO는 UDP를 지원하지 않습니다.
5. AIO 프로그래밍 방법은 "죽음 콜백"과 같이 약간 복잡합니다.