Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 

(시리즈 글이 3개 있습니다.)
스크립트: 58. 파이썬 - async/await 기본 사용법
; https://www.sysnet.pe.kr/2/0/13423

스크립트: 59. 파이썬 - 비동기 호출 함수(run_until_complete, run_in_executor, create_task, run_in_threadpool)
; https://www.sysnet.pe.kr/2/0/13426

스크립트: 71. 파이썬 - asyncio의 ContextVar 전달
; https://www.sysnet.pe.kr/2/0/13899




파이썬 - 비동기 호출 함수(run_until_complete, run_in_executor, create_task, run_in_threadpool)

지난 글에 이어,

파이썬 - async/await 기본 사용법
; https://www.sysnet.pe.kr/2/0/13423

이번에는 다양한 비동기 호출 함수에 대해 알아보겠습니다. 우선, 비동기를 수행하는 asyncio의 run 함수 먼저 볼까요? ^^

import asyncio


async def main_async():
    time.sleep(3)


asyncio.run(main_async())

run은 async 함수를 전달받아 그것의 실행이 종료될 때까지 대기하는 역할을 합니다. 사실 이 코드는 다음과 같이 바꿔 쓸 수 있습니다. (run의 실제 내부 구현과는 다를 수 있습니다.)

import asyncio


async def main_async():
    time.sleep(3)


loop = asyncio.get_event_loop()
loop.run_until_complete(main_async())  # 의미상, "await main_async()"

만약 await 예약어 호출을 async 함수 내에서만 허용한다는 규칙이 없었다면 아마도 asyncio.run 함수는 그냥 단순히 await 예약어로 대체되었을지도 모릅니다.




파이썬의 경우, 명확하게 await 호출의 대상 함수가 async이기를 요구하는데요, 그렇다면 기존에 제작된 동기 함수들을 비동기로 호출하고 싶다면 어떻게 해야 할까요?

이런 경우에 대해 asyncio는 run_in_executor 함수를 제공합니다.

import time
import asyncio


def do_sync(delay: int=0):
    time.sleep(delay)


async def main_async():
    print('async calling', time.strftime('%X'))

    loop = asyncio.get_running_loop()
    await loop.run_in_executor(None, do_sync, 3)

    print('async called', time.strftime('%X'))


asyncio.run(main_async())

""" 출력 결과
async calling 16:02:46
async called 16:02:49
"""

run_in_executor는 2번째 인자로 받은 (동기) 함수를 내부의 이벤트 루프에 태워 비동기로 실행합니다. 따라서, run_in_executor의 호출에는 await을 해야 do_sync가 완료된 후 그다음의 코드를 이어서 호출하게 됩니다.

만약, 위의 예제에서 "await loop.run_in_executor(...)" 코드를 await 없이 호출하게 되면 실행 후 종료 시점에 아래와 같은 오류가 발생합니다.

exception calling callback for 
Traceback (most recent call last):
  File "/usr/lib/python3.8/concurrent/futures/_base.py", line 328, in _invoke_callbacks
    callback(self)
  File "/usr/lib/python3.8/asyncio/futures.py", line 374, in _call_set_state
    dest_loop.call_soon_threadsafe(_set_state, destination, source)
  File "/usr/lib/python3.8/asyncio/base_events.py", line 764, in call_soon_threadsafe
    self._check_closed()
  File "/usr/lib/python3.8/asyncio/base_events.py", line 508, in _check_closed
    raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed

왜냐하면, await이 없으므로 호출 대기(설명은 '대기'라고 했지만 스레드 블록킹은 아닙니다.)를 하지 않을 것이고, 이로 인해 그대로 프로그램이 종료하면서 아직 진행 중인 callback 함수(위의 경우, do_sync)가 강제 종료되므로 예외를 통해 그 사실을 알려주는 것입니다.




자... 그럼 run_in_executor의 병렬 처리도 이전에 쓴 글과 동일한 규칙으로 코딩할 수 있습니다. 우선, 다음의 코드는 차례로 6초가 걸리지만,

await loop.run_in_executor(None, do_sync, 3)  # 3초 지나서 이후의 코드로 진행 (다시 강조하지만, 호출 측의 스레드 블록킹이 되는 것이 아닙니다.)
await loop.run_in_executor(None, do_sync, 3)  # 3초 지나서 이후의 코드로 진행
                                              # 총 6초 소요

이렇게 하면 3초 만에 결과가 나옵니다.

task1 = loop.run_in_executor(None, do_sync, 3)  # 비동기로 do_sync 함수를 전달하고 바로 제어 반환
task2 = loop.run_in_executor(None, do_sync, 3)  # 비동기로 do_sync 함수를 전달하고 바로 제어 반환

await task1  # do_sync 작업 완료 대기 (3초) 후 진행
await task2  # 위의 코드에서 3초 대기를 이미 했으므로 곧바로 제어 반환




그다음, asyncio의 create_task 함수를 볼까요?

# 직렬로 비동기 함수 호출

async def main_async():
    await asyncio.create_task(do_async(3))
    await asyncio.create_task(do_async(3))

# 병렬로 비동기 함수 호출

async def main_async():
    task1 = asyncio.create_task(do_async(3))
    task2 = asyncio.create_task(do_async(3))

    await task1
    await task2

사실 저 코드는 asyncio.create_task 호출 자체를 없애고 await으로 대체해도 동일한 동작을 합니다.

async def main_async():
    await do_async(3)
    await do_async(3)

async def main_async():
    task1 = do_async(3)
    task2 = do_async(3)

    await task1
    await task2

단지, create_task의 경우 부가적으로 name 인자와 함께 문맥 정보를 전달할 수 있는 정도의 서비스가 추가된 정도입니다. (문맥은 설명이 복잡하니 다른 글에서 한번 다루겠습니다.)




이 정도면, 비동기 호출과 관련한 예를 웬만큼 다룬 것 같은데요, 마지막으로 하나 더 남은 것이 있다면 FastAPI에서 제공하는 run_in_threadpool입니다.

from fastapi.concurrency import run_in_threadpool


def myfunc(arg_values):
    delay_number = arg_values['delay']
    time.sleep(delay_number)


@app.get("/asynctest", response_class=HTMLResponse)
async def asynctest(delay: int = 0):
    delay_arg = {'delay': 1}
    await run_in_threadpool(myfunc, delay_arg)  # 1초 후 아래 코드로 진행
    await run_in_threadpool(myfunc, delay_arg)  # 1초 후 아래 코드로 진행
    await run_in_threadpool(myfunc, delay_arg)  # 1초 점유 (총 3초 소요)

    return "TEST"

run_in_threadpool의 이름처럼, run_in_executor와 같이 동기 함수를 비동기에 태워서 실행할 수 있는데, 하지만 동작 자체는 매우 다릅니다. 왜냐하면, run_in_threadpool에 전달한 시점에 myfunc가 호출되지 않고, await을 한 시점에 실질적인 코드 실행 단계로 넘어가기 때문입니다.

따라서, run_in_threadpool의 경우에는 await만으로는 "병렬 처리"가 되지 않습니다.

task1 = run_in_threadpool(myfunc, delay_arg)  # 작업만 전달 (실행은 안 됨)
task2 = run_in_threadpool(myfunc, delay_arg)  # 작업만 전달 (실행은 안 됨)
task3 = run_in_threadpool(myfunc, delay_arg)  # 작업만 전달 (실행은 안 됨)

await task1  # 이 단계에서 myfunc 작업을 비동기로 실행
await task2  # task1이 완료된 후 task2가 이어서 비동기로 실행
await task3  # task2가 완료된 후 task3이 이어서 비동기로 실행

대신 run_in_threadpool의 작업을 병렬 처리하고 싶다면 asyncio.gather 등의 함수를 이용할 수 있습니다.

await asyncio.gather(task1, task2, task3)  # task1, task2, task3 작업이 모두 한꺼번에 비동기로 실행

한 가지 유의해야 할 점은, run_in_threadpool의 경우 비동기 함수를 인자로 받아도 실행 시 오류가 발생하지 않는다는 점입니다.

@app.get("/asynctest", response_class=HTMLResponse)
async def asynctest(delay: int = 0):
    task1 = run_in_threadpool(my_aio_func)  # 비동기 함수를 인자로 전달
    task2 = run_in_threadpool(my_aio_func)
    task3 = run_in_threadpool(my_aio_func)

    await asyncio.gather(task1, task2, task3)


async def my_aio_func(args=None):
    print('my_aio_func: start:', args)
    await asyncio.sleep(1)
    return "my value(async)"

실제로 위의 코드를 실행해 보면 아무런 오류가 발생하지 않는데, 여기서 문제는 my_aio_func이 아예 실행이 안 된다는 점입니다. (실행해 보시면, 화면 출력에 "my-aio_func: start"라는 문자열이 안 나옵니다.)

하지만, FastAPI 앱을 종료해 보면 그제야 다음과 같은 식으로 오류 메시지를 출력합니다.

/usr/lib/python3.8/threading.py:932: RuntimeWarning: coroutine 'my_aio_func' was never awaited
  self.run()
RuntimeWarning: Enable tracemalloc to get the object allocation traceback

이 정도면... 대충 파악이 되셨을 거라 생각합니다. ^^




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 3/7/2025]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 61  62  63  64  65  66  67  68  69  70  71  [72]  73  74  75  ...
NoWriterDateCnt.TitleFile(s)
12131정성태1/27/202020089개발 환경 구성: 467. LocaleEmulator를 이용해 유니코드를 지원하지 않는(한글이 깨지는) 프로그램을 실행하는 방법 [1]
12130정성태1/26/202017431VS.NET IDE: 142. Visual Studio에서 windbg의 "Open Executable..."처럼 EXE를 직접 열어 디버깅을 시작하는 방법
12129정성태1/26/202023477.NET Framework: 882. C# - 키움 Open API+ 사용 시 Registry 등록 없이 KHOpenAPI.ocx 사용하는 방법 [3]
12128정성태1/26/202017862오류 유형: 591. The code execution cannot proceed because mfc100.dll was not found. Reinstalling the program may fix this problem.
12127정성태1/25/202017092.NET Framework: 881. C# DLL에서 제공하는 Win32 export 함수의 내부 동작 방식(VT Fix up Table)파일 다운로드1
12126정성태1/25/202018410.NET Framework: 880. C# - PE 파일로부터 IMAGE_COR20_HEADER 및 VTableFixups 테이블 분석파일 다운로드1
12125정성태1/24/202015869VS.NET IDE: 141. IDE0019 - Use pattern matching
12124정성태1/23/202017656VS.NET IDE: 140. IDE1006 - Naming rule violation: These words must begin with upper case characters: ...
12123정성태1/23/202019378웹: 39. Google Analytics - gtag 함수를 이용해 페이지 URL 수정 및 별도의 이벤트 생성 방법 [2]
12122정성태1/20/202015483.NET Framework: 879. C/C++의 UNREFERENCED_PARAMETER 매크로를 C#에서 우회하는 방법(IDE0060 - Remove unused parameter '...')파일 다운로드1
12121정성태1/20/202016243VS.NET IDE: 139. Visual Studio - Error List: "Could not find schema information for the ..."파일 다운로드1
12120정성태1/19/202018634.NET Framework: 878. C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법 - 네 번째 이야기(IL 코드로 직접 구현)파일 다운로드1
12119정성태1/17/202018819디버깅 기술: 160. Windbg 확장 DLL 만들기 (3) - C#으로 만드는 방법
12118정성태1/17/202019818개발 환경 구성: 466. C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법 - 세 번째 이야기 [1]
12117정성태1/15/202018674디버깅 기술: 159. C# - 디버깅 중인 프로세스를 강제로 다른 디버거에서 연결하는 방법파일 다운로드1
12116정성태1/15/202019305디버깅 기술: 158. Visual Studio로 디버깅 시 sos.dll 확장 명령어를 (비롯한 windbg의 다양한 기능을) 수행하는 방법
12115정성태1/14/202019517디버깅 기술: 157. C# - PEB.ProcessHeap을 이용해 디버깅 중인지 확인하는 방법파일 다운로드1
12114정성태1/13/202021370디버깅 기술: 156. C# - PDB 파일로부터 심벌(Symbol) 및 타입(Type) 정보 열거 [1]파일 다운로드3
12113정성태1/12/202021438오류 유형: 590. Visual C++ 빌드 오류 - fatal error LNK1104: cannot open file 'atls.lib' [1]
12112정성태1/12/202016634오류 유형: 589. PowerShell - 원격 Invoke-Command 실행 시 "WinRM cannot complete the operation" 오류 발생
12111정성태1/12/202020436디버깅 기술: 155. C# - KernelMemoryIO 드라이버를 이용해 실행 프로그램을 숨기는 방법(DKOM: Direct Kernel Object Modification) [16]파일 다운로드1
12110정성태1/11/202019690디버깅 기술: 154. Patch Guard로 인해 블루 스크린(BSOD)가 발생하는 사례 [5]파일 다운로드1
12109정성태1/10/202016499오류 유형: 588. Driver 프로젝트 빌드 오류 - Inf2Cat error -2: "Inf2Cat, signability test failed."
12108정성태1/10/202017351오류 유형: 587. Kernel Driver 시작 시 127(The specified procedure could not be found.) 오류 메시지 발생
12107정성태1/10/202018465.NET Framework: 877. C# - 프로세스의 모든 핸들을 열람 - 두 번째 이야기
12106정성태1/8/202019574VC++: 136. C++ - OSR Driver Loader와 같은 Legacy 커널 드라이버 설치 프로그램 제작 [1]
... 61  62  63  64  65  66  67  68  69  70  71  [72]  73  74  75  ...