개요
이 글은 비동기 프로그래밍의 개념과 관련된 내용을 정리한다
글은 다음과 같은 순서로 진행된다
- 동기와 비동기의 차이와 CPU작업과 I/O작업의 차이
- 비동기 작업을 쉽게 수행할 수 있도록 도와주는 Future의 개념과 작동 방식
- 비동기 I/O 작업과 Coroutine
동기와 비동기
비동기 방식은 호출한 작업이 완료되는 것을 기다리지 않고 바로 다음작업을 수행할 수 있다
예를들어 메인스레드에서 작업을 호출할 때, 해당 작업이 메인스레드를 블락한다면 동기작업이고
해당 작업이 메인스레드를 블락하지 않는다면 비동기 작업이다
메인스레드는 블락되지 않고 프로그램이 수행되면서, I/O 작업(네트워크 요청, 디스크 I/O 등)이나 CPU 작업(복잡한 계산)과 같이 시간이 오래 걸리는 작업을 처리할 때 유용하다
CPU작업과 I/O작업
비동기 프로그래밍 방식을 살펴보기 앞서, CPU작업과 I/O작업에 대해 정리하면 다음과 같다
CPU 작업
CPU 작업은 실제로 CPU의 연산 능력을 사용하는 작업을 의미한다
이러한 작업은 주로 데이터를 처리하거나 계산하는 데 사용되는 알고리즘을 실행하는 데 필요한 연산이다
I/O 작업
I/O 작업은 입력/출력 연산을 수행하는 작업을 의미한다
이러한 작업은 주로 디스크, 네트워크 카드, 키보드, 마우스 등의 장치와의 데이터 전송을 포함한다
DMA(Direct Memory Access)는 CPU가 아닌 별도의 하드웨어 컨트롤러가 메모리와 I/O 장치 간의 데이터 전송을 직접 관리하게 해준다
이를 통해, 데이터가 디스크나 네트워크 카드로부터 완전히 전달될 때까지 CPU가 작업을 수행할 수 있게 된다
멀티스레딩과 비동기 작업
멀티스레딩은 여러 스레드를 동시에 실행하여 CPU 작업을 병렬로 처리하는 방식으로, 비동기 프로그래밍의 한 형태다
각 스레드는 독립적인 실행 경로를 가지며, 스레드 간에는 메모리를 공유할 수 있다
이러한 멀티스레딩 환경에서 비동기 작업의 결과를 표현하고 관리하는 데 Future가 특히 유용하다
아무래도 다 비슷한 동작을 약간씩 다른 이름으로 수행하는데, C++을 기준으로 먼저 설명한다
C++에서 Future, Promise, Task
작업의 결과는 Promise를 통해 작업의 결과(올바른 값이냐 오류냐)를 설정할 수 있는 방법을 추가로 제공하면,
Future를 통해 이 작업 결과를 얻어올 수 있다(단, 작업이 완료될 때까지 결과를 가져오는 데 블로킹될 수 있다)
Task의 경우 Promise와 Callable을 감싸서 해당 작업을 취소시킬 수 있는 기능도 제공한다
JavaScript에서 Promise
자바스크립트에서 Promise는 선수겸 감독 김수겸처럼, Task이자 Future다
Promise는 비동기 작업의 최종 완료(또는 실패)와 그 결과 값을 나타내는 객체로, 자바스크립트에서 비동기 작업을 처리하는데 중요한 개념이다
다음과 같이 Promise는 .then과 .catch로 결과에 따른 동작을 지정할 수 있다
> const https = require('https');
> function fetch(url) {
... return new Promise((resolve, reject) => {
... https.get(url, (res) => {
... let data = '';
...
... res.on('data', (chunk) => {
... data += chunk;
... });
...
... res.on('end', () => {
... resolve(data);
... });
...
... }).on("error", (err) => {
... reject(err);
... });
... });
... }
fetch('<https://example.com>')
.then(data => console.log(data))
.catch(err => console.error(err));
Promise를 반환하는 작업은 async/await을 이용한 코드로 작성할 수도 있다
try {
let data = await fetch('<https://example.com>');
console.log(data);
} catch (err) {
console.error(err);
}
Swift에서 Task
Swift 5.5부터 도입된 Swift Concurrency를 통해 async/await을 이용한 비동기 프로그래밍을 지원한다
Task는 비동기 작업을 나타내며, 이 작업은 async 함수를 통해 실행된다
Task를 이용하면 작업이 완료될 때까지 기다리지 않고도 작업의 상태와 결과를 쿼리 및 취소할 수 있다
let task = Task {
let result = await someAsyncFunction()
print(result)
}
/// 취소
task.cancel()
if task.isCancelled {
print("The task was cancelled.")
}
Swift Concurrency를 통해 CPU 작업은 병렬로 실행될 수 있으며, I/O 작업은 비동기로 처리될 수 있다
내부적으로는 협력스레드를 이용하는데, 작업이 수행되면 협력 스레드를 점유하기 때문에 시간이 오래 걸리는 CPU 작업을 Swift Concurrency로 수행시킬 때는 주의해야 한다
그러나 이것이 CPU 작업을 완전히 피해야 한다는 의미는 아니다
Swift Concurrency는 작업의 우선순위를 고려하여 이러한 작업을 스케줄링하고 관리할 수 있다
Task(priority: .low) {
let result = await someAsyncFunction()
print(result)
}
다음과 같이 priority를 통해 우선순위를 부여할 수 있다
비동기 I/O 작업과 코루틴
코루틴이란
코루틴(coroutine)은 동시성을 처리하기 위한 프로그래밍 기법 중 하나다
위키피디아에선 코루틴이 다음과 같이 설명된다
코루틴(coroutine)은 루틴의 일종으로서, 협동 루틴이라 할 수 있다(코루틴의 "Co"는 with 또는 together를 뜻한다). 상호 연계 프로그램을 일컫는다고도 표현가능하다. 루틴과 서브 루틴은 서로 비대칭적인 관계이지만, 코루틴들은 완전히 대칭적인, 즉 서로가 서로를 호출하는 관계이다 … 어떠한 코루틴이 발동될 때마다 해당 코루틴은 이전에 자신의 실행이 마지막으로 중단되었던 지점 다음의 장소에서 실행을 재개한다
코루틴의 활용
CPU작업을 여러 개 수행해야 한다면, 멀티스레딩으로 구현하면 된다(CPU 작업은 결국 코루틴을 통해 스레드를 점유하기에 효율적으로 CPU를 활용 못하게 된다)
그렇지만 비동기 I/O 작업은 어떤가? 이전에 말했듯이 DMA 덕분에 데이터가 완전히 전달될 때까지 CPU는 다른 일을 할 수 있는데, 이런 경우에는 코루틴이 큰 도움이 될 수 있다
우선 코루틴은 정의에 따르면 함수가 마지막으로 중단된 위치에서 다시 시작될 수 있는데, 덕분에 비동기 작업을 쉽게 관리할 수 있다
예를들어 네트워크에서 데이터를 요청하는 함수가 있다면, 이 함수는 데이터가 도착할 때까지 기다려야 한다
이때 코루틴을 사용하면 이 함수는 데이터가 도착할 때까지 '일시 중단’되었다가, 데이터가 도착하면 함수는 '재개’되어 데이터를 처리하면 된다
코루틴과 이벤트 루프
그러나 코루틴 자체로는 여러 작업을 동시에 수행할 수 있는 메커니즘을 제공하지 않는데, 이벤트 루프를 이용하면 여러 작업을 수행시킬 수 있다
이벤트 루프는 '이벤트’를 계속해서 체크하며, 새로운 이벤트가 발생하면 적절한 핸들러를 호출한다
이때, 이벤트로는 I/O, UI, Timer, Signal과 같은 요소가 있다
코루틴이 ‘일시 중단’되면, 이벤트 루프는 다른 작업을 계속 처리하고
코루틴이 ‘재개’될 준비가 되면, 이벤트 루프는 코루틴을 다시 시작한다
이것이 가지는 장점은 코루틴 스레드에 일시정지된 문맥을 스택영역에 기록했다가, 이벤트가 발생하면 수행을 이어갈 수 있어서 Cotext Switching 비용이 발생하지 않는다는 것이다!
Python에서 asyncio
Python은 비동기 I/O 프로그래밍을 위해 asyncio와 async/await를 지원한다
Python의 표준라이브러리인 asyncio 모듈은 코루틴을 기반으로 동작한다
asyncio코루틴은 async def로 정의되며, await 키워드를 사용하여 Future 객체나 다른 코루틴을 기다린다
import asyncio
async def fetch_data():
print("데이터 요청 중...")
await asyncio.sleep(2) # 네트워크 요청 시뮬레이션
print("데이터 도착")
return "데이터"
async def main():
data = await fetch_data()
print(data)
asyncio.run(main())
마무리
Future나 Task의 어원이 궁금했었는데 잘 알게되었고, 이전에 사용했었던 언어들에서 어떻게 비동기 작업을 했었는지 정리할 수 있는 좋은 기회였다
또한, Python은 언급된 다른 언어들과 다르게 코루틴을 통해 비동기 동작을 수행하는데
기존에 멀티스레딩을 이용하여 비동기 동작을 수행시킬 때에 비해 어떤 이점을 가지는지 알 수 있게 되었다
Swift Concurrency의 경우, 사실 약간 이것저것 다 섞어놓은 방식으로 협력 스레드를 이용하는데
코루틴과 비슷한 방식으로 동작되도록 구현했다는 말을 이해할 수 있게 되었다(지금까지는 내부적으로 코루틴을 이용하는 줄 알았다..)
'CS' 카테고리의 다른 글
공유기는 라우터인가? 스위치인가? (0) | 2024.12.01 |
---|---|
Observer Pattern (0) | 2024.08.24 |