Skip to content

1. 비동기 프로그래밍 소개

I/O는 프로그램의 흐름에 큰 짐이 될 수 있다. 커널에 연산을 수행하도록 요청한 다음, 그 작업이 끝낱 때까지 기다려야 한다. 이는 실제 읽기 연산을 하는 주체가 여러분의 프로그램이 아니라 커널이기 때문이다.

우리가 I/O 연산 대부분이 CPU보다 수십 배 이상 느린 장치에서 이뤄어진다. 따라서 커널과의 통신이 빨라도, 커널 장치에서 결과를 가져와서 우리에게 전달하기까지는 상당한 시간이 걸린다.

비동기 I/O를 사용하면 I/O 연산이 완료되기를 기다리는 동안 다른 연산을 수행하여 이런 유휴 시간을 활용할 수 있다.

동시성 프로그램 은 단일 스레드에서 실행하기에, 전통적인 다중 스레드 프로그램보다 작성과 관리가 쉽다. 동시성 함수는 모두 같은 메모리 공간을 공유하므로 이런 함수 간에 데이터를 공유하므로 이런 함수 간에 데이터를 공유할 때는 일반적인(변수를 통한) 방법을 사용해도 예상대로 작동한다. 하지만 언제 어느 줄의 코드가 메모리 공간을 사용할지 알 수 없으므로 경쟁 상태(race condition)를 조심해야 한다.

비동기 프로그래밍 소개

일반적으로 프로그램이 I/O 대기에 들어가면, 실행이 멈추고 커널이 I/O 요청과 관련한 저수준 연산을 처리하며(컨텍스트 스위칭)

I/O 연산이 끝날 때까지 프로그램은 재개되지 않는다. 나중에 다시 실행을 하락받으면 마더보드에서 프로그램을 다시 초기화하고 재개를 위한 준비 작업을 수행해야 한다.

반면에 동시성 프로그램은 보통 실행할 대상과 시점을 관리하는 이벤트 루프를 사용한다. 근본적으로 이벤트 루프는 실행할 함수의 목록에 지나지 않는다. 목록 맨 앞의 함수가 실행되고, 그 다음에 두 번째 함수가 실행되는 식이다.

from queue import Queue

# 전역 변수로 이벤트 루프 선언
eventloop = None

# Queue 클래스를 상속받아 EventLoop 클래스 구현
class EventLoop(Queue):
    # 이벤트 루프를 시작하는 메서드
    def start(self):
        while True:
            # 큐에서 함수를 하나 꺼내 실행
            function = self.get()
            function()

# "Hello"를 출력하고 다음 이벤트로 "do_world" 함수를 큐에 추가
def do_hello():
    global eventloop
    print("Hello")
    eventloop.put(do_world)

# "world"를 출력하고 다음 이벤트로 "do_hello" 함수를 큐에 추가
def do_world():
    global eventloop
    print("world")
    eventloop.put(do_hello)

# 메인 실행 부분
if __name__ == "__main__":
    # 이벤트 루프 객체 생성
    eventloop = EventLoop()

    # 첫 번째 이벤트로 do_hello 함수 큐에 추가
    eventloop.put(do_hello)

    # 이벤트 루프 시작
    eventloop.start()

이벤트 루프를 비동기 I/O 연산과 결합하면 I/O 작업을 수행할 때 엄청난 성능 형상을 얻을 수 있다.

위 코드는 eventloop.put(do_world) 호출은 do_world 함수에 대한 비동기 호출을 대략적으로 보여준다. 이 연산은 nonblocking 이라고 부른다.

즉, 즉시 반환되지만 나중에 do_world 함수를 호출함을 보장 한다는 말이다. 비슷하게 비동기 함수 안에 네트워크 쓰기 연산이 있으면 실제 쓰기가 끝나지 않았어도 함수는 즉시 반환된다. 문자열 쓰기가 끝나면 이벤트가 발생해서 프로그램이 이를 알 수 있다.

이벤트 루프와 비동기 I/O 라는 두 개념을 한데 모으면, 요청한 I/O 연산이 끝나기를 기다리는 동안 다른 함수를 실행하는 프로그램을 만들 수 있다.

Note

함수에서 함수로 전환하는 작업도 비용이 든다. 커널이 실행할 함수를 메모리에 준비해야 하고, 캐시 상태도 원래 예측과 달라질 것이다. 따라서 프로그램에 I/O 대기가 많을 때 동시성에서 얻는 이익이 가장 커진다. 컨텍스트 스위칭에 비용이 들지만, I/O 대기 시간을 활용해서 얻는 이익이 훨씬 크기 때문이다.