비동기 입출력
"오늘의AI위키"의 AI를 통해 더욱 풍부하고 폭넓은 지식 경험을 누리세요.
1. 개요
비동기 입출력(I/O)은 I/O 요청 후 완료 여부를 즉시 확인하지 않고 콜백이나 신호 등의 별도 메커니즘을 통해 확인하는 방식을 의미한다. 블로킹/논블로킹, 동기/비동기 개념과 구분되며, 일반적으로 논블로킹 방식으로 동작하지만 항상 그런 것은 아니다. 비동기 I/O는 버퍼 처리, 입출력 성패 확인, 시스템 콜 종료 등의 특징을 가지며, 실시간 운영체제, 엔터프라이즈 환경, 이벤트 드리븐 프레임워크 등 다양한 환경에서 사용된다. 구현 방식은 프로세스 분산, 폴링, select 루프, 콜백, 스레드, 파이버, 완료 큐 등 운영체제, 프로그래밍 언어, 라이브러리에 따라 다양하다. 자바, C++, C#, 파이썬, 자바스크립트 등 여러 프로그래밍 환경에서 비동기 프로그래밍을 지원하며, 리눅스와 윈도우 등 운영체제에서도 구현된다.
비동기 입출력(I/O)은 논블로킹 I/O와 매우 자주 혼동되지만, 동기/비동기, 블로킹/논블로킹이라는 분류는 반드시 일치하지 않는다. POSIX 환경에서 `O_NONBLOCK`이 설정된 파일 디스크립터에 대해 일반적인 `read`나 `write`를 수행하면 논블로킹이 되지만, "블록될 것 같으면 에러로 한다"는 동작이므로 비동기가 되는 것은 아니다.[1] (대부분의 I/O 작업은 운영체제(OS) 내의 버퍼 등에 의해, 동기형 API에서도 블록되지 않고 완료될 수 있는 경우도 많다).[1]
다양한 운영체제, 프로그래밍 언어, 라이브러리에서 비동기 I/O를 구현하는 여러 방식이 존재한다. 주요 구현 방식은 다음과 같다:
Java 1.5에서 표준화된 나 C++11에서 표준화된 `std::async`/`std::future` 등은 스레드 기반의 Future를 실현한다.[3][4] .NET Framework/.NET Core에서는 C# 등 .NET 언어에서 사용할 수 있는 Task Parallel Library|태스크 병렬 라이브러리영어 (TPL)와 Microsoft Visual C++의 동시 실행 런타임에서는 C++에서 사용할 수 있는 Parallel Patterns Library|병렬 패턴 라이브러리영어 (PPL)이 제공된다.[5]
I/O를 읽는 세 가지 접근 방식의 예시이다. 객체와 함수는 추상적이다.[1]
2. 블로킹/논블로킹, 동기/비동기
비동기 I/O는 다음과 같은 특징을 갖는다.[1]
# 버퍼의 내용이 커널 등에 의해 복사되거나, 또는 프로그래머의 책임으로 처리가 완료될 때까지 요구하는 프로세스가 그것을 유지해야 한다.[1]
# (권한 위반 등, 즉시 커널이 에러 등으로 할 수 있는 경우를 제외하고) 입출력의 성패도, 입출력을 요구하는 시스템 콜의 결과로는 얻을 수 없으며, 콜백 또는 다른 시스템 콜 등으로 다시 얻을 필요가 있다.[1]
# 위와 같은 제한 하에, 입출력 요구의 시스템 콜은 블록되지 않고, 최소한의 처리로 즉시 종료된다.[1]
이러한 API 스타일에 의한 I/O가 비동기 I/O이다. 따라서 비동기 I/O가 이용되는 것은, "시간 제약이 엄격한 RTOS이기 때문에"와 같은 이유가 아니다.[1] 상호 배제 제어의 사정 등으로 블록시킬 수 없거나, 또는 성능상의 이유로 엔터프라이즈 용도로 요구되는 경우도 있으며, 이벤트 드리븐 형 프레임워크이기 때문에 필요하다는 경우도 있다.[1]
비동기 입출력 형태와 POSIX 함수의 예시는 다음과 같다.블로킹 논블로킹 비동기 API write, read write, read + poll / select aio_write, aio_read
모든 형태의 비동기 입출력은 잠재적인 자원 충돌과 관련된 실패의 가능성을 열어둔다. 이를 방지하기 위해서는 상호 배제, 세마포어 등을 사용하는 주의 깊은 프로그래밍이 필요하다.
3. 구현 방식
일반적인 컴퓨팅 하드웨어는 폴링과 인터럽트, 두 가지 방법으로 비동기 I/O를 구현한다. DMA는 폴링 또는 인터럽트당 더 많은 작업을 수행하는 수단이다.
리눅스에서는 POSIX-XSI, POSIX 1003.1b, io_uring 구현이 이루어지고 있다.
윈도우에서는 Windows NT 3.1 이후 모든 버전에서 구현이 이루어지고 있다. 윈도우 API는 비동기 버전 함수를 제공하며, "I/O 완료 포트"를 통해 최적의 워커 스레드 수 제어와 I/O 오프로드를 구현할 수 있다.
4. 비동기 프로그래밍 환경
이를 발전시켜 C# 5.0/VB.NET 11 이후, Python 3.5 이후에는 async/await 구문이 제공된다. F#에는 비동기 워크플로(asynchronous workflow)라고 불리는, TPL과는 다른 독자적인 인프라를 이용한 비동기 프로그래밍 기능이 있다. C++에서는 C++20에서 `co_await` 구문이 표준화되었다.[6]
JavaScript는 싱글 스레드로 동작하므로 비동기 프로그래밍은 이벤트 구동 기반의 유사한 방식에 의존했지만, Web Worker에 의해 멀티 스레드 프로그래밍이 지원된다. (Web Worker의 스레드는 메모리 공간을 공유하지 않고, 실제로는 메시지 기반의 멀티 프로세스 프로그래밍이다.) ECMAScript 2015 (ES2015)에서는 Promise가, ES2017에서는 async/await 구문이 표준화되었다.
5. 예제
```python
device = IO.open()
ready = False
while not ready:
print("읽을 데이터가 없습니다!")
ready = IO.poll(device, IO.INPUT, 5) # 5초가 경과하거나 읽을 데이터가 있는 경우(INPUT) 제어를 반환한다.
data = device.read()
print(data)
```
```python
ios = IO.IOService()
device = IO.open(ios)
def inputHandler(data, err):
"입력 데이터 처리기"
if not err:
print(data)
device.readSome(inputHandler)
ios.loop() # 모든 작업이 완료될 때까지 대기하고 모든 적절한 처리기를 호출합니다.
```
```python
ios = IO.IOService()
device = IO.open(ios)
async def task():
try:
data = await device.readSome()
print(data)
except Exception:
pass
ios.addTask(task)
ios.loop() # 모든 작업이 완료될 때까지 대기하고 모든 적절한 처리기를 호출합니다.
```
```python
device = IO.open()
reactor = IO.Reactor()
def inputHandler(data):
"입력 데이터 처리기"
print(data)
reactor.stop()
reactor.addHandler(inputHandler, device, IO.INPUT)
reactor.run() # 이벤트를 처리하고 적절한 처리기를 호출하는 리액터를 실행합니다.
5. 1. 블로킹, 동기
python
device = IO.open()
data = device.read() # 장치에 데이터가 있을 때까지 스레드가 차단됩니다.
print(data)
```
이 방식은 I/O 장치에 데이터가 도착할 때까지 스레드(프로그램의 실행 흐름)가 차단(block)되어 다른 작업을 수행하지 못하고 대기하는 방식이다. 데이터가 도착하면 `device.read()` 함수가 데이터를 반환하고, `print(data)` 문이 실행되어 데이터를 출력한다. 이 방식은 코드가 간결하고 이해하기 쉽지만, I/O 작업이 완료될 때까지 다른 작업을 수행할 수 없다는 단점이 있다.
5. 2. 블로킹 및 논블로킹, 동기
```python
device = IO.open()
data = device.read() # 장치에 데이터가 있을 때까지 스레드가 차단된다.
print(data)
```
```python
device = IO.open()
ready = False
while not ready:
print("읽을 데이터가 없습니다!")
ready = IO.poll(device, IO.INPUT, 5) # 5초가 경과하거나 읽을 데이터가 있는 경우(INPUT) 제어를 반환한다.
data = device.read()
print(data)
```
```python
ios = IO.IOService()
device = IO.open(ios)
async def task():
try:
data = await device.readSome()
print(data)
except Exception:
pass
ios.addTask(task)
ios.loop() # 모든 작업이 완료될 때까지 대기하고 모든 적절한 처리기를 호출한다.
```
```python
device = IO.open()
reactor = IO.Reactor()
def inputHandler(data):
"입력 데이터 처리기"
print(data)
reactor.stop()
reactor.addHandler(inputHandler, device, IO.INPUT)
reactor.run() # 이벤트를 처리하고 적절한 처리기를 호출하는 리액터를 실행한다.
```
비동기 I/O는 논블로킹 I/O와 매우 자주 혼동되지만, 동기 또는 비동기, 블로킹 또는 논블로킹이라는 분류는 반드시 일치하지 않는다. POSIX 환경에서 `O_NONBLOCK`이 설정된 파일 디스크립터에 대해 일반적인 read나 write를 수행하면 논블로킹이 되지만, "블록될 것 같으면 에러로 한다"는 동작이므로, 비동기가 되는 것은 아니다 (대부분의 I/O 작업은 OS 내의 버퍼 등에 의해, 동기형 API에서도 블록되지 않고 완료될 수 있는 경우도 많다).
비동기 I/O는 다음과 같은 스타일의 입출력 API에 의한 I/O이다.
# 버퍼의 내용이 커널 등에 의해 복사되거나, 또는 프로그래머의 책임으로 처리가 완료될 때까지 요구하는 프로세스가 그것을 유지해야 한다.
# (권한 위반 등, 즉시 커널이 에러 등으로 할 수 있는 경우를 제외하고) 입출력의 성패도, 입출력을 요구하는 시스템 콜의 결과로는 얻을 수 없으며, 콜백 또는 다른 시스템 콜 등으로 다시 얻을 필요가 있다.
# 이상과 같은 제한 하에, 입출력 요구의 시스템 콜은 블록되지 않고, 최소한의 처리로 즉시 종료된다.
이러한 비동기 I/O가 이용되는 경우는 "시간 제약이 엄격한 RTOS이기 때문에"와 같은 이유뿐만 아니라, 상호 배제 제어의 사정 등으로 블록시킬 수 없거나, 성능상의 이유로 엔터프라이즈 용도로 요구되는 경우, 또는 이벤트 드리븐 형 프레임워크이기 때문에 필요한 경우 등 다양하다. 별도의 스레드를 사용하여, 프로세스 내에서 비동기 I/O처럼 보이게 하는 라이브러리(프레임워크) 등도 있을 수 있다.
5. 3. 논블로킹, 비동기
논블로킹, 비동기 방식은 입출력(I/O) 작업이 완료될 때까지 기다리지 않고 즉시 제어를 반환하는 방식이다. 이 방식은 다음과 같은 특징을 가진다.
다음은 논블로킹, 비동기 I/O를 구현하는 예시이다.
```python
ios = IO.IOService()
device = IO.open(ios)
def inputHandler(data, err):
"입력 데이터 처리기"
if not err:
print(data)
device.readSome(inputHandler)
ios.loop() # 모든 작업이 완료될 때까지 대기하고 모든 적절한 처리기를 호출합니다.
```
위 코드에서 `device.readSome()` 함수는 논블로킹 방식으로 데이터를 읽는다. 데이터가 준비되지 않아도 즉시 반환되며, 데이터가 준비되면 `inputHandler`라는 콜백 함수가 호출되어 결과를 처리한다. `ios.loop()`는 모든 비동기 작업이 완료될 때까지 대기하는 역할을 한다.
Async/await를 사용하면 동일한 예시를 다음과 같이 작성할 수 있다.
```python
ios = IO.IOService()
device = IO.open(ios)
async def task():
try:
data = await device.readSome()
print(data)
except Exception:
pass
ios.addTask(task)
ios.loop() # 모든 작업이 완료될 때까지 대기하고 모든 적절한 처리기를 호출합니다.
```
Reactor 패턴을 사용한 예시는 다음과 같다.
```python
device = IO.open()
reactor = IO.Reactor()
def inputHandler(data):
"입력 데이터 처리기"
print(data)
reactor.stop()
reactor.addHandler(inputHandler, device, IO.INPUT)
reactor.run() # 이벤트를 처리하고 적절한 처리기를 호출하는 리액터를 실행합니다.
```
비동기 I/O는 논블로킹 I/O와 혼동되기도 하지만, 엄밀히 말하면 동기/비동기와 블로킹/논블로킹은 반드시 일치하는 개념은 아니다. POSIX 환경에서 `O_NONBLOCK` 플래그를 설정하여 파일을 읽거나 쓰는 것은 논블로킹이지만, "블록될 것 같으면 에러로 처리"하는 동작이므로 비동기는 아니다.
비동기 I/O는 다음과 같은 특징을 갖는 입출력 API를 사용한다.
1. 프로세스는 버퍼의 내용이 커널 등에 의해 복사되거나, 프로그래머의 책임으로 처리가 완료될 때까지 버퍼를 유지해야 한다.
2. 입출력의 성공 여부는 입출력을 요구하는 시스템 콜의 결과로 즉시 얻을 수 없으며, 콜백 또는 다른 시스템 콜을 통해 확인해야 한다.
3. 입출력 요구 시스템 콜은 블록되지 않고 최소한의 처리만으로 즉시 종료된다.
비동기 I/O는 "시간 제약이 엄격한 RTOS이기 때문에" 사용되는 것은 아니다. 상호 배제 제어 문제로 블록될 수 없거나, 성능상의 이유로 엔터프라이즈 환경에서 요구되거나, 이벤트 드리븐형 프레임워크에서 필요하기 때문에 사용될 수 있다. 별도의 스레드를 사용하여 프로세스 내에서 비동기 I/O처럼 동작하는 라이브러리도 존재한다.
5. 4. Async/Await (Python)
다음은 Async/await를 사용한 파이썬 예시이다.
```python
ios = IO.IOService()
device = IO.open(ios)
async def task():
try:
data = await device.readSome()
print(data)
except Exception:
pass
ios.addTask(task)
ios.loop() # 모든 작업이 완료될 때까지 대기하고 모든 적절한 처리기를 호출합니다.
```
위 코드는 I/O를 읽는 세 가지 접근 방식 중 논블로킹, 비동기 방식을 보여준다. 객체와 함수는 추상적이다.[1]
다음은 Reactor 패턴을 사용한 예시이다.
```python
device = IO.open()
reactor = IO.Reactor()
def inputHandler(data):
"입력 데이터 처리기"
print(data)
reactor.stop()
reactor.addHandler(inputHandler, device, IO.INPUT)
reactor.run() # 이벤트를 처리하고 적절한 처리기를 호출하는 리액터를 실행합니다.
```[1]
6. 리눅스에서의 샘플 프로그램
c
/* 비동기 I/O를 사용한 파일 출력 예시 */
#include
#include
#include
#include
#include
#include
#include
#define DATA_BUF_SIZE 4096
#define DATA_BUF_NUM 128
int main(void)
{
int fd;
int n, status;
unsigned char *Aio_buff[DATA_BUF_NUM];
struct aiocb Aiocb[DATA_BUF_NUM];
struct aiocb *List[DATA_BUF_NUM];
if ((fd = open("datafile", (O_CREAT | O_WRONLY), 0666)) < 0)
{
exit(1);
}
/* 링크 리스트 생성 */
for (n = 0; n < DATA_BUF_NUM; n++)
{
Aio_buff[n] = (unsigned char*)memalign(sysconf(_SC_PAGESIZE), DATA_BUF_SIZE);
memset((void *)(Aio_buff[n]), n, DATA_BUF_SIZE); /* 데이터는 페이지 단위로 n으로 채움 */
Aiocb[n].aio_buf = Aio_buff[n];
Aiocb[n].aio_offset = (long long)(DATA_BUF_SIZE * n);
Aiocb[n].aio_nbytes = (long long)DATA_BUF_SIZE;
Aiocb[n].aio_fildes = fd;
Aiocb[n].aio_reqprio = 0;
Aiocb[n].aio_lio_opcode = LIO_WRITE;
Aiocb[n].aio_sigevent.sigev_notify = SIGEV_NONE;
List[n] = &Aiocb[n];
}
/* 비동기 I/O 발행 */
lio_listio(LIO_WAIT, (struct aiocb **)&List[0], DATA_BUF_NUM, &sig);
/**/
/* 이 사이에 파일 출력과 병행하여 다른 처리를 기술할 수 있다. */
/**/
/* 비동기 I/O 종료 대기 */
aio_suspend((const struct aiocb **)&List[0], DATA_BUF_NUM, &timeout);
/* 에러 상태 확인 */
for (n = 0; n < DATA_BUF_NUM; n++)
{
status = aio_error(&(Aiocb[n]));
if (status) printf("%d is error %d:%s\n", n, status, strerror(status));
}
/* 종료 상태 확인 */
for (n = 0; n < DATA_BUF_NUM; n++)
{
status = aio_return(&(Aiocb[n]));
if (status != DATA_BUF_SIZE) printf("%d is write error\n", n);
}
close(fd);
return 0;
}
참조
[1]
웹사이트
Ringing in a new asynchronous I/O API
https://lwn.net/Arti[...]
2020-07-27
[2]
웹사이트
Registered Input-Output (RIO) API Extensions
https://technet.micr[...]
2016-08-31
[3]
웹사이트
非同期プログラミング - C# | Microsoft Docs
https://docs.microso[...]
[4]
웹사이트
非同期プログラミングの一般的概念 - ウェブ開発を学ぶ | MDN
https://developer.mo[...]
[5]
문서
Windowsランタイム環境では、タスク完了後の実行コンテキストの自動復帰がサポートされるなど、PPLはTPLに近い動作仕様となる。
[6]
문서
Visual C++ 2015で実験的に実装されていたawait構文が、C++標準に取り込まれた。
본 사이트는 AI가 위키백과와 뉴스 기사,정부 간행물,학술 논문등을 바탕으로 정보를 가공하여 제공하는 백과사전형 서비스입니다.
모든 문서는 AI에 의해 자동 생성되며, CC BY-SA 4.0 라이선스에 따라 이용할 수 있습니다.
하지만, 위키백과나 뉴스 기사 자체에 오류, 부정확한 정보, 또는 가짜 뉴스가 포함될 수 있으며, AI는 이러한 내용을 완벽하게 걸러내지 못할 수 있습니다.
따라서 제공되는 정보에 일부 오류나 편향이 있을 수 있으므로, 중요한 정보는 반드시 다른 출처를 통해 교차 검증하시기 바랍니다.
문의하기 : help@durumis.com