[Python] 코루틴 (Coroutine)

2023. 2. 16. 10:44

코루틴을 공부할수록 헷갈렸던 개념들까지 정리하고자 한다.

 

🟡 동기 (synchronous) 

  • 요청을 보냈을 때 응답이 돌아와야 다음 동작 수행
  • A 작업이 모두 끝날 때까지 B 작업은 대기

 

🟡 비동기 (asynchronous)

  • 응답 상태와 상관없이 다음 동작 수행
  • A 작업과 B 작업을 동시에 실행
  • multiprocessing, multithreading, asyncio 라이브러리를 통해 구현 가능

🟡 동시성 (Concurrency)

  • 동시에 작업을 수행하는 것이 아닌, 동시에 하는 것 처럼 느껴지게 하기 위한 시분할 처리
  • 각 작업을 번갈아가며 실행 (A|B|A|B|A|B|A ...)
  • 작업 시간은 (A : 5분 + B : 3분 = 8분)으로 단축 없이 동일하지만 사용자는 동시에 일어나는 것 처럼 보임
  • 동시성에는 문맥 교환이 발생
  • 코루틴, 싱글 코어에서 멀티 쓰레드로 구현 가능

↓문맥 교환 알아보기

더보기

문맥 교환(context switching) : 현재 CPU를 사용중인 프로세스의 CPU 제어권이 다른 프로세스로 이양되는 과정

현재 프로세스(A)는 CPU 사용시간이 끝나면 스케줄링 알고리즘에 의해 다음 프로세스(B)한테 CPU를 넘겨줘야 함

이 때 현재 프로세스(A)는 문맥을 자신의 PCB(Process Control Block, 프로세스 제어 블록)에 저장하게 되고 다음 프로세스(B)는 전에 저장했던 자신의 문맥을 PCB로부터 실제 하드웨어로 복원 시키는 과정이 필요함

 

각 프로세스는 프로세스 이미지를 가짐

 

프로세스 이미지 구성

  • 프로세스 제어 블록(PCB)
    • 프로세스의 모든 정보들이 들어있음 (스케줄링, 자원 할당, 인터럽트 처리 등)
  • 텍스트 (코드, 명령어)
  • 데이터
  • free area
  • 스택

 

(위에 말이 맞다, 하지만 조금은 틀리다! 동시성 개념때문에 정말 헷갈렸다.. 밑에서 이어가도록 하자)

 

🟡 병렬성 (Parallelism)

  • 정말로 동시에 여러 작업을 병렬로 수행
  • CPU 코어(자원)당 한 개의 일을 함. 즉, 멀티 코어일 경우에 해당
  • 작업 시간은 A : 5분, B : 3분일 경우 5분으로 시간 단축
  • 멀티 코어에서 멀티 쓰레드로 구현 가능

🔸 쓰레드와 코루틴 차이점

쓰레드와 코루틴 모두 비동기 작업을 하기 위해 사용된다.

 

[ Thread A, B, C 가 있을 경우 ]

A를 수행하다가 B의 결과가 필요할 때, A는 블로킹이 되고 B로 문맥교환이 발생한다.

 

[ Coroutine A, B, C 가 있을 경우 ]

A를 진행하던 중 B가 실행되어도 실행중인 Thread를 정지하면서 기존 Thread에서 B를 실행한다.

 

따라서 Thread는 문맥교환의 비용이 발생하고

Coroutine은 문맥교환 없이 해당 루틴을 일시 중단해 기존 Thread 기법보다 비용이 적게 든다는 것이다.

 

일명 루틴이라고 불리는 Non-Blocking job을 정의한 뒤 멀티태스킹을 수행하는 것이 Coroutine이다.

Non-Blocking : 현재 작업을 수행중인 Main Thread를 중단(Blocking)하지 않고, 백그라운드에서 작업을 수행하여 현재 Thread를 종료하지 않도록 하는 비동기적 작업 수행


I/O bound와 CPU bound

앞서 동시성은 작업을 번갈아가며 진행하고 시간은 단축되지 않았다.

하지만 코루틴 예제를 찾아보다가 작업들의 실행이 겹치고 이로 인해 시간이 단축됨을 확인했다.

코루틴을 통해 동시성 프로그래밍을 할 수 있는데 어떻게 실행이 겹칠 수 있는지 개념적으로 헷갈렸다.

이는 CPU가 작업에 따라 어떻게 할당되는지를 알아야 한다.

 

컴퓨터가 수행하는 하나의 작업은 CPU 작업(CPU Bounded Task) I/O 작업(I/O Bounded Task)으로 구분된다.

 

🟡 I/O bound

출처 : https://realpython.com/python-concurrency

작업 수행 시 CPU보다 I/O 작업이 많은 경우를 의미한다.

I/O bound 작업은 대부분 DB 데이터 송수신, 과 관련된 작업이다.

그림을 보면 실제 CPU 작업이 수행되는 시간은 파란색 뿐이다. 나머지 빨간색은 I/O 작업이 완료될 때까지 기다리는 시간이다. 따라서 이 시간동안 CPU는 쉬고 있다.

속도를 높이려면 CPU가 쉬는 시간에 일을 해야 한다.

 

🟡 CPU bound

출처 : https://realpython.com/python-concurrency

작업 수행 시  I/O보다 CPU 작업이 많은 경우를 의미한다.

네트워크와 통신하거나 파일에 액세스 하지 않고 연산을 수행하는 프로그램과 같다.

프로그램의 속도에 영향을 끼치는 것이 네트워크나 파일 시스템이 아니라 오로지 CPU이다.


헷갈렸던 부분을 생각해보자.

 

예를 들어 A 작업을 코루틴에 등록해 실행하면

B 작업이 코루틴에 등록됐을 때 A 작업이 끝날 때까지 await이 발생해 대기하게 된다.

하지만 A 작업이 CPU를 사용하는 연산인 경우에만 파이썬의 작업이 할당된다. A 작업에서 네트워크 I/O나 파일 I/O와 같이 CPU 연산이 아닌 외부 입출력 작업을 할 때는 다른 작업이(B 작업)이 CPU에 할당될 수 있다.

따라서 이런 순간 동시에 작업이 처리가 되고 시간이 단축된다. (파이썬은 여전히 하나의 작업만 하는 것임)

 

I/O 작업이 일어나는 함수 앞에 await을 붙이면 효율적이다.


synchronous & asynchronous & block & non-block

코루틴을 적용하려면 알아야하는 개념이 있다.

앞선 동기 비동기를 다른 말로 정의해보자.

 

[ 호출되는 함수의 작업 완료 여부를 처리하는 주체의 차이 ]

🔸 동기 (synchronous)

  • 호출된 함수의 수행 결과 및 종료를 호출한 함수가 처리함
  • 호출하는 함수가 호출되는 함수의 작업 완료 후 리턴을 기다리거나 호출되는 함수로부터 바로 리턴을 받더라도 작업 완료 여부를 호출하는 함수가 계속 확인하며 신경씀

🔸 비동기 (asynchronous) 

  • 호출된 함수의 수행 결과 및 종료를 호출된 함수가 직접 신경 쓰고 처리함
  • 호출되는 함수에게 callback을 전달해서, 호출되는 함수의 작업이 완료되면 호출되는 함수가 전달 받은 callback을 실행하고, 호출하는 함수는 작업 완료 여부를 신경쓰지 않음

 

[ 호출되는 함수가 바로 리턴하는지의 차이 ] 

🟡 Block

 호출된 함수가 자신이 할 일을 모두 마칠 때까지 제어권을 계속 갖고 호출한 함수에게 바로 돌려주지 않음

 

🟡 Non-Block

호출된 함수가 자신이 할 일을 마치지 않더라도 바로 제어권을 건네주어(return) 호출한 함수가 다른 일을 할 수 있음

 

출처 : https://homoefficio.github.io/2017/02/19/Blocking-NonBlocking-Synchronous-Asynchronous/

주의 : NonBlocking-Async 방식을 쓰는데 그 과정 중에 하나라도 Blocking으로 동작하는 일이 포함되어 있다면 의도하지 않게 Blocking-Async로 동작할 수 있음

 

↓더 알아보기

더보기

비동기 처리시 blocking되니 비동기 호출에서 async를 붙여 코루틴으로 구현하고

코루틴 객체가 CPU 연산을 하고 있을 때 해당 객체에 다시 접근하려 하면 blocking되어 있어 연산이 끝날 때까지 기다려야 된다.

따라서 파이썬에서 효율적으로 구현하기 위해서는 async로 구현된 객체가 blocking을 피할 수 있도록 multiprocessing을 할당해줘야 한다.

ex) 웹 프레임 워크의 worker(=프로세스)가 blocking되어 효율적으로 처리되지 못하는 코루틴 객체를 멀티 프로세싱으로 동작시켜 효율성을 올리는 것이다.


예시

예를 들어 웹페이지를 가져오는 코드를 짜야하는데

urlopen은 blocking함수라서 asyncio로 구현하는 효과가 크지 않음

 

방법 1

따라서 blocking함수를 멀티  쓰레드를 사용해 병렬로 실행시켜야 함

이 때 주의해야할 점이 있다.

 

파이썬에만 존재하는 GIL이라는게 있다.

GIL은 Python 인터프리터는 한 시점에 하나의 쓰레드에 의해서만 실행됨을 의미한다.

그러면 파이썬에서는 멀티 쓰레딩을 못하는 것으로 생각할 수 있지만 아니다.

출처 : https://it-eldorado.tistory.com/160

위 그림처럼 여러 쓰레드가 병렬로 실행 될 수 없다는 의미일 뿐 멀티 쓰레딩은 가능하다.

 

이 역시 I/O bound에서 좋은 결과를 보인다.

CPU를 사용하지 않는 시간에 다른 쓰레드를 실행하기 때문이다.

 

따라서 CPU bound에서 병렬성 연산은 수행할 수 없으며,

sleep이나 위에 언급한 urlopen 등 외부 I/O 작업을 할 경우에 멀티 쓰레딩은 유용하다.

 

CPU bound 에서는 멀티 프로세싱을 이용해 GIL을 우회할 수 있다.

 

방법 2

멀티 스레딩을 사용하지 않고 asyncio + aiohttp를 사용하는 것이다.

aiohttp는 파이썬 request가 동기로 요청을 처리하기 때문에 이를 비동기로 http 요청을 처리하기 위한 라이브러리다.

멀티 스레딩, asyncio 차이점

https://superfastpython.com/threadpoolexecutor-vs-asyncio

 

ThreadPoolExecutor vs. AsyncIO in Python

You can use ThreadPoolExecutor for ad hoc IO-bound tasks and AsyncIO for asynchronous programming generally or for vast numbers of IO-bound tasks. In this tutorial, you will discover the difference…

superfastpython.com

 

 

 

 

 

 

더 알아볼 것

- 멀티 프로세싱

- subprocess