파이썬과 싱글스레드
그냥 문득
코드치다가 갑자기 궁금해서 await와 스레드간의 관계에 대해 고민을 해봤다. 그런데 생각의 흐름이 막히는 부분들이 있어서 중간중간 찾아본김에 정리해보려 한다.
파이썬과 GIL
파이썬은 일단 별도의 설정 없이는 싱글 스레드 기반이다.
혹시 레디스처럼 뒷단에서는 뭔가 멀티스레드로 처리하는건 아닌가 싶어서 찾아봤는데 GIL 메커니즘 때문에 기본적으로 한 번에 하나의 스레드만 실행된다고 한다. 결국 여러 스레드를 생성해도 어느 특정 한 시점의 스냅샷을 찍으면 언제나 한 개의 스레드만 실행된다는 것이다.
그런데 여기서부터 뭔가 의문 투성이었다. 많은 사람들이 성능 향상을 위해 async 같은 비동기 키워드를 사용한다고 들었는데, 이게 어떤 의미를 가지는지 감이 오지를 않았다.
이 작업이 완료되는동안 스레드가 블로킹되지 않는다는 것에 의의를 두는 것은 알겠는데, 그러면 그 시간동안 다른 작업을 처리하는데 더 집중할 수 있다는 건가? 해당 작업은 일단 await 하는채로?? 그런데 싱글 스레드 기반인데 다른 작업에 시간을 더 할애한다는 게 어떻게 가능한거지??? 뭔가 컨텍스트 스위치 대상에서 얘를 빼버리는걸까????
이런 의문들이 들어서 실실 검색을 해봤다.
이벤트 루프
이 구조를 본 순간 이미 대부분의 의문이 풀렸다. 그래도 나중에 똑같은 고민을 할 수 있으니 최대한 잘 적어봐야겠다.
파이썬의 asyncio는 싱글스레드 기반의 비동기 실행 라이브러리이다. 어떻게 보면 자바스크립트와도 작동 구조가 유사한 점이 있는데, 태스크 큐에서 작업을 가져온 후 처리하는 방식이다. 이때 await 키워드를 만나면 실행을 일시 중단하고 제어권을 이벤트 루프에 반환한다. 그리고 이벤트 루프는 다른 작업을 실행한다.
파이썬 비동기는 cpu 작업에는 큰 효과가 없고, 네트워크/파일 io 작업에서는 효율이 크게 증가한다는 의미가 여기서 나온 듯 하다. cpu 중심 작업은 아무리 await를 사용해도 io 작업과는 달리 연산을 위해 스레드를 계속 점유하기 때문에 별 이점이 없다.
반면에 네트워크 요청, 파일 입출력, 데이터베이스 쿼리처럼 대기 시간이 대부분의 작업인 경우에는 cpu 자원이 크게 필요하지 않기 때문에 비동기 처리가 효과적이다. 응답을 기다리면서 이벤트 루프는 다른 작업을 실행하기 때문에 cpu가 놀지 않는 것이다.
코드로 살펴보기
1
2
3
4
5
6
7
8
9
import asyncio
async def main():
print("Start")
await asyncio.sleep(1)
print("End")
asyncio.run(main())
run을 통해 main 함수를 실행할 때 새로운 이벤트 루프를 생성한다. main() 코루틴은 최상위 태스크로 이벤트 루프에 등록되고, 비동기 작업의 스케줄링과 실행이 시작된다.
start가 출력되고, await 키워드를 본 순간 코루틴(asyncio.sleep)이 완료될 때까지 현재 실행 중인 코루틴(main)의 제어권을 이벤트 루프에 반환한다.
이벤트 루프는 main 코루틴의 실행을 일시 중단하고, 다른 태스크를 실행하거나 대기 상태에 들어간다.
실행이 완료되면 완료 신호가 이벤트 루프로 전달되고, 이벤트 루프는 main 함수를 깨워서 이전의 await 지점부터 실행한다.