Data science/Python

[5분 컷 이해] 코루틴, 메인루틴, 서브루틴 !

연금(Pension)술사 2024. 3. 3. 23:20

요약


  • 메인루틴(Main routine): 메인루틴은 보통 프로그램의 시작점이며, 프로그램의 주 흐름을 담당합니다. 메인루틴은 일련의 작업을 수행하고 다른 서브루틴이나 코루틴을 호출할 수 있습니다. 프로그램이 시작되는 메인코드라고 생각하면 됩니다.
  • 서브루틴(subroutine): 메인루틴에서 호출되는 함수, 또는 서브루틴에서 호출되는 함수들을 의미합니다. 즉, 다른 루틴에서 호출되는 경우를 의미합니다.
  • 코루틴(Coroutine): 메인루틴에서 호출되지만, 코루틴은 실행되는 도중에, 일시중단되어 다시 메인루틴으로 전환되었다가 다시 코루틴으러 전환될 수 있는 "제어흐름"이 가능한 루틴을 의미합니다.

 

코루틴, 메인루틴, 서브루틴은 프로그래밍에서 중요한 개념이며, 이를 이해하는 것은 프로그래밍의 성능을 향상시키는 데 도움이 됩니다. 각각의 이유를 자세히 설명하겠습니다.

  1. 프로그램의 구조 이해(메인루틴/서브루틴의 이해):
    • 메인루틴은 프로그램의 주요 흐름을 담당하며, 프로그램의 시작점입니다. 이를 이해함으로써 프로그램이 어떻게 실행되는지 전반적으로 이해할 수 있습니다.
    • 서브루틴은 재사용 가능한 작은 조각의 코드를 나타냅니다. 이를 통해 코드를 더 모듈화하고 유지 보수하기 쉽게 만들 수 있습니다.
  2. 코드의 재사용과 모듈화(서브루틴/코루틴의 이해):
    • 서브루틴을 사용하면 비슷한 작업을 수행하는 코드를 여러 곳에서 재사용할 수 있습니다. 이로써 코드의 중복을 피하고 유지보수성을 향상시킬 수 있습니다.
    • 코루틴은 서브루틴과 유사하지만, 일시 중단되고 다시 시작될 수 있기 때문에 작업의 흐름을 제어하는 데 유용합니다. 이를 통해 비동기 작업이나 상태 유지가 필요한 작업을 효율적으로 처리할 수 있습니다.
  3. 동시성과 비동기 프로그래밍(코루틴의 의해):
    • 코루틴은 동시성과 비동기 프로그래밍에서 중요한 역할을 합니다. 코루틴을 이용하면 여러 작업을 동시에 실행하고 작업 간에 상태를 공유할 수 있습니다. 이를 통해 복잡한 동작을 효과적으로 조정하고 성능을 향상시킬 수 있습니다.
  4. 효율적인 코드 실행:
    • 코루틴은 작업의 일시 중단과 다시 시작을 가능하게 함으로써 I/O 바운드 작업을 효율적으로 처리할 수 있습니다. 이를 통해 대기 시간이 긴 작업을 실행하는 동안 다른 작업을 수행할 수 있으며, 시스템 자원을 더 효율적으로 활용할 수 있습니다.

이러한 이유들로 인해 코루틴, 메인루틴, 서브루틴을 이해하는 것은 프로그래밍에서 중요한 역할을 합니다. 이러한 개념을 잘 숙지하고 활용함으로써 보다 효율적이고 유연한 코드를 작성할 수 있습니다.

 

메인루틴(Main routine)


메인루틴은 프로그램의 주요 흐름을 담당하는 부분으로, 일반적으로 프로그램의 시작점이며 종료 지점이 될 수도 있습니다. 메인루틴은 전체 프로그램의 실행 흐름을 정의하고 조절하며, 다른 서브루틴이나 함수를 호출하여 작업을 수행하고 그 결과를 처리합니다.

메인루틴은 보통 다음과 같은 역할을 수행합니다:

  1. 프로그램의 시작점: 메인루틴은 일반적으로 프로그램이 실행되면 가장 먼저 실행되는 부분입니다. 프로그램의 시작 지점을 정의하고 초기화 작업을 수행합니다.
  2. 주요 작업 수행: 메인루틴은 프로그램이 수행해야 하는 주요 작업을 정의하고 호출합니다. 이는 사용자의 입력을 받는, 데이터를 처리하는, 다른 모듈이나 서브루틴을 호출하는 등의 작업을 포함할 수 있습니다.
  3. 서브루틴 호출: 메인루틴은 필요에 따라 다른 서브루틴이나 함수를 호출하여 작업을 분할하고 모듈화합니다. 이를 통해 코드를 재사용하고 유지보수성을 향상시킬 수 있습니다.
  4. 프로그램의 종료: 메인루틴은 프로그램이 종료되는 지점을 나타냅니다. 모든 주요 작업이 완료된 후 정리 작업을 수행하고, 필요에 따라 리소스를 해제하고 프로그램을 종료합니다.

간단한 예시로 메인루틴을 파이썬코드로 구현해보겠습니다. 아래와 같이 input으로 이름, 나이를 입력받아 출력하는 프로그램입니다. 다른 모듈에서 호출되는 것 없이 main()함수가 호출되고 종료됩니다. 이 main함수에서 프로그램이 시작되고, 종료되기에, "프로그램의 흐름이 정의"되었다라고 말할 수 있습니다. 즉, main()이 메인루틴이 됩니다.

def main():
    print("프로그램 시작")

    # 사용자 입력 받기
    name = input("이름을 입력하세요: ")
    age = int(input("나이를 입력하세요: "))

    # 정보 출력
    print("입력된 정보:")
    print("이름:", name)
    print("나이:", age)

    # 종료 메시지 출력
    print("프로그램 종료")

if __name__ == "__main__":
    main()

 

서브루틴(Subroutine): 메인루틴이나 서브루틴에서 호출되는 함수


서브루틴은 다른 프로그램에서 호출되는 (=피호출)되는 함수를 의미합니다. 예는 아래와 같습니다. 위의 예시와 같이 main()이 호출되고 종료되기에 메인루틴은 main()이라는 함수입니다. 그리고 이 main()내에서 calculate_sum()이 호출되고 종료됩니다. 즉, main인 메인루틴에 의해서 호출되는 calculate_sum이 서브루틴입니다. 

def main():
    print("메인루틴 시작")
    result = calculate_sum(3, 4)
    print("결과:", result)
    print("메인루틴 종료")

def calculate_sum(a, b): # 서브루틴
    return a + b

if __name__ == "__main__":
    main()

시간에 흐름에 따라, 두 루틴을 시각화하면 아래와 같습니다. 메인루틴이 서브루틴을 호출하고, 그 동안 메인루틴은 서브루틴이 일을 다 처리할 때까지, 잠시 작업대기를 합니다. 그리고, 서브루틴의 일이 끝나면, 메인루틴으로 다시 작업이 넘어가서 메인루틴의 테스크가 종료됩니다.

Figure 1. 시간의 흐름에 따른 메인루틴-서브루틴

 

코루틴(Coroutine): "일시중지 후 재시작"이 가능한 함수


코루틴은 일시 중단되고 다시 시작될 수 있는 함수로, 일반적인 함수와 다르게 실행 중에 일시 중단되어 값을 주고 받을 수 있습니다. 이를 통해 비동기적인 작업이나 상태를 유지하는 작업을 효율적으로 처리할 수 있습니다. 특히, 코루틴은 서브루틴과 다르게 메인루틴으로 작업상태가 넘어가도, 코루틴내 내용을 잃지않고 유지할 수 있습니다. 그리고, 이런 메인루틴->코루틴, 코루틴->메인루틴의 작업상태를 넘겨 실행하는 단계를 "진입점"이라고합니다. 이런 진입점은 서브루틴은 "메인루틴->서브루틴"의 1개의 진입점을 가지지만, 코루틴은 여러 개의 진입점을 가집니다.

 

코루틴을 사용하려면, 코루틴의 성격을 정리해보겠습니다. 

  • 코루틴은 generator의 특별한 형태입니다. (generator도 데이터를 yield와 함께 하나씩 얻어올 수 있고, 그 다음 차례가 그대로 유지되기에 코루틴으로 취급합니다.)
  • 코루틴은 파이썬의 2.5부터 generator로 설명되지만, 실제로 async, await구문과 함께쓰려면 3.5의 이상부터 필요합니다.

그리고, 코루틴의 시작/값보내기/값얻기의 구현방법을 알아야합니다.

  • 코루틴의 시작: 코루틴은 보통 while True 구문을 이용해서 계속해서 일반적으로 코루틴의 시작을 알립니다.
  • 코루틴에 값보내기: 코루틴에 값을 보내는 방법은 코루틴객체.send(객체)로 외부의 값을 코루틴으로 전달합니다.
  • 코루틴 내에서 값 얻기: 이 값을 가져오기 위해서, `yield` 키워드를 이용하여 값을 얻어옵니다.

 

아래의 예시에서는 `searcher()`가 코루틴 함수입니다. 하나 씩 내용을 확인해보겠습니다.

  1. while구문으로 코루틴을 계속 실행하는 상태로 둡니다.
  2. while 구문이 이하가 도는 경우, yield구문에서 값이 올 때까지 기다립니다.
  3. next()가 호출되어야하는 이유는 제너레티너나 코루틴을 실행하고 첫번째 값을 얻기위해 호출합니다. 코루틴은 yield 표현식이 보이면 일시중단됩니다.
  4. 코루틴객체.send(객체)의 구문으로 코루틴에 값을 보내면, 그 보내는것과 동시에 코루틴이 실행됩니다. 다시 while True 구문에 의해서 yield 키워드가 등장하고, 코루틴이 일시중지됩니다. 이와 동시에 다시 메인루틴으로 작업이 옮겨갑니다.

 

def searcher():
    """
    검색어를 받아들이고 출력하는 코루틴
    """
    while True:
        query = yield  # 일시 중단하고 외부로부터 값을 받음
        print("검색어:", query)

# 코루틴 생성
search = searcher()
next(search)  # 코루틴 시작

# 코루틴에 값 전달
search.send("파이썬")
search.send("데이터 분석")

Figure2. 메인루틴-코루틴의 시간에 엔트리포인트 예

또, 다른 예시입니다. 코루틴은 일시정지를 함으로, 코루틴 내의 그 내용을 잃어버리지 않고 유지할 수 있다고 했습니다. 코루틴에서 average, count, total라는 변수를 계속 코루틴에서 들고 있고, send로 더할 때 마다, 코루틴의 변수가 변경되는 것을 확인할 수 있습니다.

def running_average():
    """
    입력된 숫자들의 평균을 계산하는 코루틴
    """
    total = 0
    count = 0
    average = None

    while True:
        value = yield average  # 새로운 값 받기
        total += value
        count += 1
        average = total / count

# 코루틴 생성
calc_average = running_average()
next(calc_average)  # 코루틴 시작

# 코루틴에 숫자 전달하고 평균 계산
print("평균:", calc_average.send(10))  # 출력: 10.0
print("평균:", calc_average.send(20))  # 출력: 15.0
print("평균:", calc_average.send(30))  # 출력: 20.0

 

 

코루틴을 이용한 async, await의 활용


asyncio을 사용해서 비동기적으로 작업을 수행할 수 있습니다. asyncio을 사용하기위해 대표적인 키워드 2개를 살펴보겠습니다.

      • async 키워드: async def로 함수를 정의하면 해당 함수는 코루틴 함수로 선언됩니다. 이는 함수가 비동기적으로 실행될 수 있음을 나타냅니다. 아래와 같이, async def 로 정의된 함수는 인자를 담아 호출하면, coroutine object로 정의됨을 알 수 있습니다.

    • await 키워드: await 키워드는 비동기 작업이 완료될 때까지 대기하고, 해당 작업의 결과를 반환합니다. await 있는 키워드가 실행되면, 이 라인의 구문이 실행되는 동안, "비동기적으로 메인루틴으로 잡이 넘어가고", "완료되면 메인루틴의 작업에서 다시 코루틴으로 작업이 넘어옵"니다.  Q. "async함수를 정의하면 꼭 await구문을 써야하냐?"할수도있는데, 꼭 사용하지 않아도 됩니다만, 비동기작업을 안쓸꺼면 굳이 async로 정의할 필요는 없을 것 같습니다
async def hello():
    print("안녕하세요, 코루틴!")

async def main():
    print("메인 루틴 시작")
    await hello()
    print("메인 루틴 종료")

asyncio.run(main())

 

아래의 예시를 보겠습니다.

  1. `async def`로 fetch_url이라는 함수를 비동기적인 함수로 정의했습니다. 
  2. `await` 키워드를 fetch_url내에 정의합니다. ` fetch_url `이 호출되고 await구문이 있는 라인이 실행되면 sleep이 실행되면서, 작업을 메인루틴으로 넘깁니다.
  3. 메인루틴이 실행중이다가, await구문이 완료되면 그 다음인 return으로 반환됩니다.
  4. `async main`에서는 await asyncio.gather로 비동기작업을 여러개로 구성할 때, 모든 작업이 완료될 때까지 대기해주는 함수입니다. 
import asyncio

async def fetch_url(url):
    print(f"URL 가져오는 중: {url}")
    await asyncio.sleep(1)  # 비동기적으로 1초 동안 대기
    print(f"URL 가져오기 완료: {url}")
    return f"Content of {url}"

async def main():
    tasks = [fetch_url("https://www.example.com"), fetch_url("https://www.google.com")]
    completed_tasks = await asyncio.gather(*tasks)
    print("모든 작업 완료:")
    for result in completed_tasks:
        print(result)

asyncio.run(main())

 

언제쓰면 유용한가? "네트워크 요청", "file I/O bound task", "데이터베이스 쿼리"


  1. 동시성 증가: 비동기 프로그래밍을 사용하면 여러 I/O 작업을 동시에 처리할 수 있습니다. 한 작업이 완료될 때까지 기다리지 않고 다음 작업을 시작할 수 있습니다.
  2. 응답 시간 개선: I/O 바운드 작업을 동시에 처리함으로써 전체 시스템의 응답 시간을 개선할 수 있습니다. 작업이 완료될 때까지 기다리는 동안 다른 작업을 수행하여 전체 처리 시간을 최소화할 수 있습니다.
  3. 자원 효율성: 비동기 프로그래밍을 사용하면 블로킹되는 대기 시간 동안 다른 작업을 수행할 수 있으므로 시스템 자원을 더 효율적으로 활용할 수 있습니다.
  4. 스케일링 용이성: 비동기 프로그래밍을 사용하면 더 많은 동시 요청을 처리할 수 있으며, 이로써 시스템의 스케일링이 용이해집니다.
반응형