Asyncio 없이 싱글 스레드 논 블락킹 비동기 서버 만들기 (Feat. Event Loop 이해하기)

퀄슨 개발팀(Qualsondev)
Qualson Tech Blog
Published in
29 min readJul 11, 2024

안녕하세요. 퀄슨에서 백엔드 개발을 하고 있는 박종인 입니다.

웹 서버를 개발함에 있어, 동일한 리소스로 더 많은 요청을 처리하기 위하여, 점점 더 많은 곳에서 비동기 방식으로 개발을 하고 있습니다.

저희 퀄슨에서는 내부적으로 이러한 비동기 처리, 그중에서도 특히 현재 대부분의 경우에 사용하고 있는 이벤트 루프 기반의 비동기 처리 방식에 대한 이해도를 높이고자 이와 관련된 세미나를 자체적으로 준비하여 진행하였습니다.

그리고 이 세미나의 내용이 저희 회사 외의 다른 분들께도 이벤트루프의 동작 방식을 이해하는데 도움을 드릴 수 있을 것 같아, 이와 같이 관련 내용을 공유하기로 하였습니다.

이 포스트에서는 파이썬의 asyncio 라이브러리와 await & async 문법을 사용하지 않으면서 직접 socket을 이용하여비동기적으로 요청을 받는 간단한 서버를 만들어보고, 이 과정에서 asyncio 라이브러리의 핵심 요소인 event loop의 원리에 대해서 알아보도록 하겠습니다.

mindmajix, 2023

1. 그냥 Telnet 서버

처음에는 간단하게 Telnet 클라이언트로부터 요청을 받으면 그 요청내용을 단순히 echo 해주는 서버를 만들어보겠습니다.

1.1. 일단 요청만 잘 받아보자

다음 코드는 12345번 포트에 TCP 연결 요청을 받는 서버입니다. Telnet 클라이언트로 12345번 포트에 요청을 해보면 runserver 함수 안에 있는 코드에 의해 요청이 처리됩니다.

# basic_server.py
import socket


def runserver():
# Create a listening socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(('0.0.0.0', 12345))
server_socket.listen()

connection_socket, client_address = server_socket.accept()
print(f"Connection established with {client_address}")

data = connection_socket.recv(1024)
print(data)
connection_socket.close()


if __name__ == "__main__":
runserver()

1.1.1. 동작방식

위 코드를 한 줄 씩 살펴보도록 하겠습니다.

1. 서버에서 사용할 소켓을 생성합니다.

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

socket.AF_INET 는 IPv4 주소체계를 의미합니다.

socket.SOCK_STREAM 은 TCP를 의미합니다.

즉 위 코드는 IPv4 주소체계를 사용하는 TCP 소켓을 생성하여 server_socket 에 할당합니다.

2. 실습의 편의성을 위해 소켓에 간단한 옵션을 추가합니다.

server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

socket.SOL_SOCKET 은 소켓 레벨에서 설정을 정의 하겠다는 의미입니다. 이 인자의 자리에는 socket.IPPROTO_IP 를 넣어서 IP 레벨의 설정을 하거나 socket.IPPROTO_TCP 를 넣어서 TCP 레벨의 설정을 할 수 있는데, 이 이상의 설명은 본 포스팅에서 벗어나는 주제 이므로 넘어가도록 하고, 일단 지금은 소켓에 설정을 하겠다 라는 의도를 담은 인자라고 생각해주시면 되겠습니다.

socket.SO_REUSEADDR 는 이 소켓이 주소를 재사용 한다는 의미입니다. 여기서 주소란 IP, PORT쌍을 의미합니다. 이 옵션을 사용하므로써 우리가 서버를 켜고 끌때 Address already in use 에러를 방지합니다.

1 은 위 socket.SO_REUSEADDR 옵션을 enable 하겠다는 flag 입니다.

3. 소켓을 0.0.0.0 주소의 12345번 포트에 바인딩 합니다.

server_socket.bind(('0.0.0.0', 12345))

0.0.0.0 은 서버컴퓨터에 있는 모든 네트워크 인터페이스를 지칭하는 특별한 IP입니다. 즉 (‘0.0.0.0’, 12345) 는 이더넷, 와이파이등 모든 네트워크 인터페이스를 통해 12345번 포트로 들어오는 통신을 소켓에서 담당 하겠다는 것을 의미합니다.

로컬에서만 테스트를 하시길 원한다면, 루프백 인터페이스이자 로컬호스트를 가리키는 127.0.0.1 로 해도 상관없습니다.

4. 소켓에서 LISTEN을 시작합니다.

server_socket.listen() 

이제 이 컴퓨터의 12345번 포트로 들어오는 TCP 연결을 받을 준비가 완료되었습니다.

5. 요청이 들어올때 까지 기다립니다.

connection_socket, client_address = server_socket.accept()

이 서버에 12345번 포트로 TCP 연결 요청이 들어오면 연결을 성사시키고 connection_socket 객체와 클라이언트의 주소(IP, PORT)를 반환합니다.

서버에 연결 요청이 들어오기 전까지는 다음 코드라인으로 넘어가지 않습니다.

6. 요청을 처리하고 커넥션을 닫습니다.

data = connection_socket.recv(1024)
print(data)
connection.close()

7. server_sockerconnection_socket은 아래 그림애서 각각 welcoming socket과 connection socket을 표방합니다.

James F. Kurose & Keith W. Ross, 2022

서버가 우리가 설정한 12345 포트에 바인딩 된 소켓에 요청을 받으면, 별도의 소켓을 하나 더 생성하고 그 소켓에서 클라이언트와 데이터를 주고받게 됩니다. 이 별도의 소켓이 바로 connection_socket 입니다.

1.1.2. 실행 해보기

1. 위 코드를 basic_server.py 라는 이름의 파일로 저장하고 터미널에서 실행시켜봅시다.

python basic_server.py

2. 터미널을 하나 더 열어서 Telnet 클라이언트를 로컬호스트 12345에 연결합니다.

telnet localhost 12345

# 다음과 같은 stdout을 확인할 수 있음
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.

3. Telnet 클라이언트상에서 메시지를 하나 날려봅니다.

hi
Connection closed by foreign host.

hi 라는 메시지를 날리면 connection_socket, client_address = server_socket.accept() 의 코드가 진행되고 data = connection.recv(1024)에서 hi 를 읽어 data에 할당하고 print(data) 를 통해 hi를 basic_server.py가 실행되고 있는 터미널에 출력합니다.

최종적으로 connection_socket.close() 라인에서 커넥션을 종료하면 터미널에 Connection closed by foreign host. 라는 메시지와 함께 연결이 종료됩니다.

1.1.3. 개선해야 할 부분

1. 현재로써는 한 커넥션만 처리하고 바로 connection_socket.close() 를 호출하기 때문에 Telnet 클라이언트에서 여러 메시지를 보내도 커넥션을 닫지 않도록 하면 좋을 것 같습니다.

2. 현재로써는 Telnet 클라이언트 터미널에서 보면 자신이 타이핑한 hi만 볼 수 있는데, echo 서버를 만들려면 서버측에서 클라이언트로부터 받은 hi를 다시 클라이언트로 보내는 코드가 필요합니다.

1.2. echo 서버를 만들어보자

위 코드에서 한 커넥션만 처리하고 커넥션을 닫는 문제와 커넥션에서 보낸 메시지를 echo하는 기능을 추가한 서버의 코드는 다음과 같습니다.

# echo_server.py
import socket


def runserver():
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(('0.0.0.0', 12345))
server_socket.listen()

while True:
connection_socket, client_address = server_socket.accept()
print(f"Connection established with {client_address}")

while True:
data = connection_socket.recv(1024)
print(data)
msg = b'echo: '
msg += data
connection_socket.send(msg)

if data == b'bye\r\n':
connection_socket.close()
break


if __name__ == "__main__":
runserver()

1.2.1. 바뀐 부분

바뀐 부분만 한번 살펴보겠습니다.

data = connection_socket.recv(1024)
print(data)
connection_socket.close()

기존에 한번만 데이터를 받고 커넥션을 받는 로직에서,

while True:
data = connection_socket.recv(1024)
print(data)
msg = b'echo: '
msg += data
connection_socket.send(msg)

if data == b'bye\r\n':
connection_socket.close()
break

bye 라는 데이터가 올 때 까지 루프를 돌면서 커넥션으로부터 데이터를 받고 그 메시지를 다시 send 하는 로직으로 바뀌었습니다.

이 업데이트로 다음과 같이 기능이 개선되었습니다.

  1. while 루프를 사용하므로써 여러 메시지를 보내도 커넥션이 종료되지 않음(1.1.2의 1 해결)
  2. telnet 클라이언트로 메시지로 보내면 응답으로 보냈던 메시지를 다시 보내줌(1.1.2의 2 해결)
  3. telnet로 bye 를 보내서 클라이언트가 커넥션을 끊을 수 있음.

1.2.2. 실행 해보기

1. 코드를 실행시킵니다.

python echo_server.py

2. 터미널을 하나 더 열어서 다음 커맨드를 실행합니다.

telnet localhost 12345

# 다음과 같은 stdout을 확인할 수 있음
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.

3. 1.1.2.의 3.과 달리 이제는 hi를 여러번 날릴 수 있습니다.

hi
echo: hi
hi
echo: hi
hi
echo: hi
hi
echo: hi
hi
echo: hi

4. bye로 커넥션을 종료합니다.

bye
echo: bye
Connection closed by foreign host.

2. 여러 커넥션을 처리하는 논 블락킹 비동기 echo 서버

python의 wsgi 구현체들은 커넥션을 생성할 때 결국에는 위와 같은 방식으로 동작합니다

benoitc, 2014

여기에 멀티스레딩 혹은 멀티프로세싱을 이용해서 동시에 여러 커넥션을 받습니다. 하지만 우리의 목적은 한 프로세스에서 여러 요청을 비동기적으로 받을 수 있는 서버를 만드는 것입니다. 그리고 이 방식은 asgi의 구현체들이 동작하는 방식입니다. 이제부터 그 방식을 방향으로 서버를 점차 개선해보겠습니다.

2.1. 코드를 논블락킹 으로 바꾸기

기존의 서버가 한 커넥션만 받을 수 있는 이유는 다음 코드에서 Telnet 요청이 올 때 까지 프로그램이 멈춰있어서 그 바깥에 있는 while 루프가 순환하지 않기 때문입니다.

while True:
connection_socket, client_address = server_socket.accept()

이렇게 한 코드가 그의 직무를 다 하기 전까지 다음 코드로 넘어가지 않는 것을 블락킹 이라고 합니다.

지금의 경우에는 위 코드에 블락킹되어 있을 때 단순히 클라이언트로부터 커넥션을 기다리고 있는 상태라 CPU는 아무런 처리를 하지 않습니다. 이는 비효율적 이므로 커넥션이 오지 않더라도 코드는 루프는 계속 순환될 수 있도록 하는것이 여러 커넥션을 비동기적으로 받기 위한 첫 걸음일 것입니다.

그리하여서, 다음과 같이 코드를 작성하면 커넥션이 오지 않더라도 일단은 while 루프가 순환하도록 만들 수 있습니다.

import socket


def runserver():
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(('0.0.0.0', 12345))
server_socket.listen()
server_socket.setblocking(False)

while True:
try:
connection_socket, client_address = server_socket.accept()
print(f"Connection established with {client_address}")
except BlockingIOError:
continue

while True:
try:
data = connection_socket.recv(1024)
except BlockingIOError:
continue

print(data)
msg = b'echo: '
msg += data
connection_socket.send(msg)

if data == b'bye\r\n':
connection_socket.close()
print(f"connection with {client_address} closed")
break


if __name__ == "__main__":
runserver()

2.1.1. 바뀐 부분

1. 서버 소켓을 생성하는 부분에 코드 한줄이 추가되었습니다.

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(('0.0.0.0', 12345))
server_socket.listen()
server_socket.setblocking(False) # 추가된 부분

소켓에 .setblocking(False) 를 설정하면 해당 소켓이 .accept().recv() 와 같이 클라이언트 측의 행동을 기대하는 함수가 호출될 때 클라이언트로 부터 아무런 행동이 없다면 BlockingIOError Exception을 내도록 합니다.

2. 클라이언트로 부터 아무런 커넥션 생성 시도가 없어 .accept() 호출이 BlockingIOError Exception을 낸다면 이를 pass 시켜서 코드가 계속 전개될 수 있도록 합니다.

while True:
try: # 추가된 부분
connection_socket, client_address = server_socket.accept()
print(f"Connection established with {client_address}")
connections.append(connection) # 추가된 부분
except BlockingIOError: # 추가된 부분
pass # 추가된 부분

3. 커넥션에서 클라이언트의 데이터를 받을때, 클라이언트가 아직 아무런 데이터를 보내지 않아 .recv(1024) 호출이 BlockingIOError Exception을 낸다면 이를 pass 시켜서 계속 코드가 전개될 수 있도록 합니다.

while True:
try: # 추가된 부분
data = connection_socket.recv(1024)
except BlockingIOError: # 추가된 부분
continue # 추가된 부분

print(data)

이로써 서버는 클라이언트로부터 아무런 행동이 없어도 while 루프를 계속해서 돌 수 있게 되었습니다. 하지만 아직까지는 하나의 커넥션만 받을 수 있습니다. 이제 여기서 한단계만 거치면 이 서버는 여러 커넥션을 받을 수 있습니다.

2.2. 여러 커넥션 처리하기

다음 코드는 2.2.1.의 코드에서 여러 커넥션을 받기 위해 개선된 코드입니다.

import socket


def runserver():
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(('0.0.0.0', 12345))
server_socket.listen()
server_socket.setblocking(False)
connections = []

while True:
try:
connection_socket, client_address = server_socket.accept()
print(f"Connection established with {client_address}")
connections.append(connection_socket)
except BlockingIOError:
pass

for connection_socket in connections:
try:
data = connection_socket.recv(1024)
except BlockingIOError:
continue

print(f"send to {client_address}: {data}")
msg = b'echo: '
msg += data
connection_socket.send(msg)

if data == b'bye\r\n':
connection_socket.close()
print(f"connection with {client_address} closed")
connections.remove(connection_socket)
break


if __name__ == "__main__":
runserver()

2.2.1. 바뀐 부분

1. 다음과 같이 여러 커넥션을 담을 리스트 선언을 추가했습니다.

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(('0.0.0.0', 12345))
server_socket.listen()
server_socket.setblocking(False)
connections = [] # 추가된 부분

2. 한 커넥션의 recv 만을 기다리던 부분을 connections 리스트를 돌며 여러 커넥션으로부터 recv 를 하도록 개선되었습니다.

for conn in connections:  # 변경된 부분
try:
data = connection_socket.recv(1024)
except BlockingIOError:
continue

3. 클라이언트에서 커넥션을 끊으면 connections 리스트에서 해당 커넥션을 삭제하는 코드를 추가했습니다.

if data == b'bye\r\n':
connection_socket.close()
print(f"connection with {client_address} closed")
connections.remove(connection_socket) # 추가된 부분
break # 추가된 부분

이로써 이 서버 프로그램은 하나의 프로세스만으로도 여러 커넥션을 받을 수 있는 비동기 서버가 되었습니다. 하지만 현재 이 프로그램은 while 루프를 쉴 새 없이 반복하며 CPU 자원을 모두 소모하는 치명적인 문제점이 있습니다. 그렇다고 하여 1초씩 쉬며 루프를 돌게하면 클라이언트는 서버가 쉬는 시간 만큼의 지연을 경험하게 됩니다. 다음 파트에서 이 문제를 해결 해보도록 하겠습니다.

3. I/O event notification을 이용한 비동기 echo 서버

3.1. I/O event 사용하기

우리가 사용하는 OS에는 어떤 파일에 읽기나 쓰기등의 I/O event 일어났을 때 프로세스에 그 event에 대해 알림을 받아볼 수 있는 API가 존재합니다.

- Linux: epoll
- MacOS: kqueue
- Windows: IOCP

파이썬에서는 selectors 라는 라이브러리를 사용하면 위 API를 사용해 볼 수 있습니다. 소켓 또한 본질적으로는 파일 이므로 위 API을 소켓에 사용할 수 있습니다. 그리하여서 소켓에 대한 I/O event를 구독하고 해당 소켓에 변경사항이 발생하면 알림을 받는식으로 구현하면 클라이언트의 행동을 기다리기위해 While 루프를 쉴세 없이 순환할 필요가 없어지게 됩니다.

먼저 selectors를 이용한 서버 구현 코드를 살펴보고, 어떻게 개선 하였는지 하나씩 살펴보도록 하겠습니다.

import selectors
import socket


def runserver():
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(('0.0.0.0', 12345))
server_socket.listen()
server_socket.setblocking(False)

selector = selectors.DefaultSelector()
selector.register(server_socket, selectors.EVENT_READ)

while True:
events = selector.select(timeout=1)

if len(events) == 0:
continue

for event, _ in events:
event_socket = event.fileobj

if event_socket == server_socket:
connection_socket, client_address = server_socket.accept()
print(f"Connection established with {client_address}")
selector.register(connection_socket, selectors.EVENT_READ)
else:
connection_socket = event_socket
data = connection_socket.recv(1024)
msg = b'echo: '
msg += data
connection_socket.send(msg)

if data == b'bye\r\n':
connection_socket.close()
print(f"connection with {client_address} closed")
break


if __name__ == "__main__":
runserver()

3.2. 바뀐 부분

1. 소켓에 I/O event를 구독하는 부분이 추가되었습니다.

selector = selectors.DefaultSelector()
selector.register(server_socket, selectors.EVENT_READ)

위 코드에서는 seletor 객체를 생성하고 그 객체가 server_socket 의 읽기 이벤트, 즉 connection 생성 요청에 관한 알림을 받도록 설정합니다.

2. selector 에 등록된 socket들로 부터 I/O event를 기다립니다.

while True:
events = selector.select(timeout=1)

if len(events) == 0:
continue

위 코드에서는 1초동안 selector객체에 등록된 소켓에 클라이언트로 부터의 요청을 기다립니다. 1초가 지나면 다음 코드로 진행이 되어 if len(events) == 0: 부분을 만나게되고 1초동안 아무런 이벤트가 없었다면 continue 로 인해 while 루프가 돌게됩니다.

이 코드 중요한 부분입니다. 앞선 코드에서는 while 루프를 쉴세없이 돌며 소켓에 클라이언트의 요청이 있는지 없는지 확인하느라 CPU 자원을 모두 소모했지만, 이 코드에서는 selector 를 통해 소켓에 클라이언트 요청이 있는지 확인하는 임무를 epoll이나 kqueue API를 통해 OS에게 위임할 수 있습니다. OS는 해당 임무를 수행할때 CPU 사용없이 효율적으로 수행합니다.

3. 만약 클라이언트로부터 요청이 있었다면 해당 소켓을 꺼냅니다.

for event, _ in events:
event_socket = event.fileobj

events 를 for문에 올리면 SelectorKey 객체의 event 와 int 타입의 file descriptor 번호의 _으로 언팩시킵니다. file descriptor 번호는 사용하지 않으므로 _ 로 언팩시켰습니다.

SelectorKey 객체의 fileobj 프로퍼티를 조회하면 앞전에서 .accept.recv 의 호출이 가능한 소켓 객체를 받아볼 수 있습니다.

여기서 받는 event_socket 은 커넥션 요청을 받은 server_socket일수도 있고 서버의 응답을 기다리는 connection_socket일 수도 있습니다.

사실 현재시점까지 보자면 connection_socket에는 아직 아무런 알림이 구독되지 않아서 event_socket이 어떻게 connection_socket일수도 있는지 이해가 어려우실 수 있습니다. 하지만 곧 이어 4에서 바로 설명을 드리겠습니다.

4. 만약 event_socketserver_socket 이라면, 이것은 커넥션 생성 요청을 의미하므로 커넥션을 받아서 connections 리스트에 추가해둡니다.

if event_socket == server_socket:
connection_socket, client_address = server_socket.accept()
print(f"Connection established with {client_address}")
selector.register(connection_socket, selectors.EVENT_READ)

위 코드에 마지막 줄을 보시면 위 1에서 server_socket 에 했던것 처럼 connection_socket 에도 I/O event를 구독을 합니다. 이로써 event_socketconnection_socket 일 수 있게 되었습니다.

5. 만약 event_socketconnection_socket 이라면, 이것은 클라이언트가 서버로 커넥션을 통해 요청을 보냈다는 의미이기 때문에 echo 응답을 보내줍니다.

else:
connection_socket = event_socket
data = connection_socket.recv(1024)
msg = b'echo: '
msg += data
connection_socket.send(msg)

6. 커넥션 닫기 요청이 오면 커넥션을 정리하는것도 잊지 않습니다.

if data == b'bye\r\n':
connection_socket.close()
print(f"connection with {client_address} closed")
break

이 개선을 통하여 CPU 자원을 지나치게 점유하지 않고도 한 서버 프로세스 만으로 여러 요청을 비동기적으로 받을 수 있는 서버를 구현해보았습니다.

4. 실제 asyncio의 Event Loop 구현과 비교하기

이번 파트에서는 위에 구현했던 서버를 토대로 asyncio의 event loop를 탐구해보겠습니다.

사실은 위에서 selectors 비동기 서버를 구현하면서 우리는 이미 간단한 event loop를 만들었습니다. 3.2의 1 부분의 코드가 바로 간소화된 event loop 입니다.

while True:
events = selector.select(timeout=1)

if len(events) == 0:
continue

...

이 아이디어를 가지고 asyncio에 event loop는 실제로 어떻게 구현되어있는지 살펴보겠습니다.

BaseEventLoop.run_forever 를 살펴보면 while: true 구문을 발견할 수 있습니다. (benoitc et al, 2023)

def run_forever(self):
"""Run until stop() is called."""

...

events._set_running_loop(self)
while True:
self._run_once()
if self._stopping:
break
...

while: true 안에 있는 self._run_once 의 정의를 살펴보겠습니다.

def _run_once(self):
"""Run one full iteration of the event loop.

This calls all currently ready callbacks, polls for I/O,
schedules the resulting callbacks, and finally schedules
'call_later' callbacks.
"""

...

event_list = self._selector.select(timeout)
self._process_events(event_list)

...

위에 발췌된 분을 보시면 asyncio 에서도 우리가 사용했던 selectors 를 사용하는 모습을 확인하실 수 있습니다.

이 모습은 결국에 우리가 3.2의 2의 코드와 흡사하게 됩니다.

while True:
events = selector.select(timeout=1)

if len(events) == 0:
continue

그 다음에 호출되는 _process_events 의 구현부분을 살펴보면 다음과 같습니다.

def _process_events(self, event_list):
for key, mask in event_list:
fileobj, (reader, writer) = key.fileobj, key.data
if mask & selectors.EVENT_READ and reader is not None:
if reader._cancelled:
self._remove_reader(fileobj)
else:
self._add_callback(reader)
if mask & selectors.EVENT_WRITE and writer is not None:
if writer._cancelled:
self._remove_writer(fileobj)
else:
self._add_callback(writer)

반복문의 앞 부분을 보면 3.2의 3 부분과 흡사한것을 보실 수 있습니다.

for event, _ in events:
event_socket = event.fileobj

asyncio의 event loop에서는 단지 read I/O event 뿐만이 아니라 더 일반적인 케이스들을 커버할 수 있도록 구현되었습니다.

이처럼 asyncio의 event loop 또한 while: true 구문안에 selectors 를 이용해 I/O event를 기다리며 비동기적인 행동을 구현한 모습을 확인할 수 있습니다.

5. 결론

지금까지 asyncio 없이 비동기 논블락킹 서버를 구현하는 방법을 살펴보고 이를통해 asyncio가 제공하는 event loop의 원리를 살펴보았습니다.

event loop란 결국엔 while: true 안에서 I/O event를 제어하는 루프란것을 알게되었습니다.

그렇다면 앞으로 더 생각해볼 부분은 asyncio의 Coroutine은 구체적으로 무엇인지일 것 입니다. 가까운 미래에 Coroutine에 관한 포스팅도 다뤄 보겠습니다.

인용자료:

benoitc. (2014, October 25). optimize the sync worker. https://github.com/benoitc/gunicorn/commit/4c601ce447fafbeed27f0f0a238e0e48c928b6f9

benoitc, et al. (2023, December 7). n.d. https://github.com/benoitc/gunicorn/blob/9802e21f779d9f1f208a1a3288218bd5b843ad46/gunicorn/workers/sync.py

James F. Kurose & Keith W. Ross. (2022). Computer Networking A Top-Down Approach (8th edition). Pearson.

mindmajix, (2023, April 4), Express JS Interview Questions https://mindmajix.com/express-js-interview-questions

--

--

퀄슨 개발팀(Qualsondev)
Qualson Tech Blog

퀄슨 개발팀의 문화와 기술에 대해서 모두에게 공유드립니다 :)