ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 파이썬 비동기 연습
    Python/이것저것 파이썬 2022. 8. 5. 13:52
    반응형

    0. 먼저 이 영상을 보고 개념을.. 

    자바스크립트로 예를 들었지만, 개념 설명이 좋은 것 같습니다... 

    https://www.youtube.com/watch?v=m0icCqHY39U 

    1. 파이썬에서의 비동기를 잘 정리한 블로그..

    https://kukuta.tistory.com/345

    동기 프로그래밍을 비동기 프로그래밍으로 바꾸는 과정, 
    이 과정에서 필요한 키워드인
    async, await, asyncio.run(), asyncio.gather(), asyncio.get_event_loop(), run_in_executor()
    를 모두 알려주네요. 읽기 편합니다.   

    • import asyncio 
      # 비동기 라이브러리 임포트
    • def 대신 async def로 함수 선언 
      # 이렇게 선언된 함수를 (네이티브) 코루틴이라고 함.
    • await
      # 비동기 함수의 결과를 기다려야 할 때 사용.
    • asyncio.gather()
      # 여러 비동기 함수를 한번에 등록
    • asyncio.run()
      # 비동기 함수 실행
    • asyncio.get_event_loop() 
      # 이벤트 루프 만듦
    • [루프].run_in_executor()
      # 동기 함수를 [루프]에 태워 (비동기적으로 실행)

     

    원글의 코드는 다음과 같습니다.. 

    import time
    import requests
    import asyncio  # asyncio 모듈 임포트
    
    
    async def download_page(url):  # async def로 함수 정의
        loop = asyncio.get_event_loop()  # 이벤트 루프 객체 얻기
        req = await loop.run_in_executor(None, requests.get, url)  # 동기함수를 비동기로 호출
    
        html = req.text
        print("complete download:", url, ", size of page(", len(html), ")")
    
    
    async def main():
        await asyncio.gather(
            download_page("https://www.python.org/"),
            download_page("https://www.python.org/"),
            download_page("https://www.python.org/"),
            download_page("https://www.python.org/"),
            download_page("https://www.python.org/")
        )
    
    
    print(f"stated at {time.strftime('%X')}")
    start_time = time.time()
    asyncio.run(main())
    finish_time = time.time()
    print(f"finish at {time.strftime('%X')}, total:{finish_time - start_time} sec(s)")

    파이썬의 공식 문서들을 보면서 이것저것 만져봅니다. 

    https://docs.python.org/ko/3/library/asyncio.html

    https://docs.python.org/ko/3/library/asyncio-api-index.html

    https://docs.python.org/ko/3/library/asyncio-task.html

    초심자답게 고수준 API 위주로 만져 봅시다. 

     

    3. 연습

    연습 삼아 조금 수정해보았습니다.  
    내장 라이브러리를 사용하는 게 편할 것 같았고.  
    루프 선언을 하나로 모으는 것도 가능할 것 같아서...  

    asyncio.all_tasks()를 이용해서 완료되지 않은 태스크를 확인해 봅니다.

    import time
    from urllib import request  # 
    import asyncio
    
    
    def req(url):
        with request.urlopen(url) as response:
            html = response.read()
        return html
    
    
    async def download_page(loop, url):
        # print(len(asyncio.all_tasks()))
        html = await loop.run_in_executor(None, req, url)
        print("complete download:", url, ", size of page(", len(html), ")")
        # print(len(asyncio.all_tasks()))
        
    
    async def main():
        loop = asyncio.get_event_loop()  # 
        await asyncio.gather(
            download_page(loop, "https://www.python.org/"),
            download_page(loop, "https://www.python.org/"),
            download_page(loop, "https://www.python.org/"),
            download_page(loop, "https://www.python.org/"),
            download_page(loop, "https://www.python.org/")
        )
    
    
    print('start')
    start_time = time.time()
    asyncio.run(main())
    print(f'finish, total:{time.time() - start_time} sec(s)')

    run_in_executor의 첫 번째 인수는 executor인데 함수를 실행시켜줄 스레드 풀 또는 프로세스 풀입니다. 
    None을 넣으면 기본 스레드 풀을 사용합니다.
    두 번째 인수는 실행할 함수, 세 번째 인수부터는 실행할 함수에 들어갈 인수입니다.

    start
    complete download: https://www.python.org/ , size of page( 50024 )
    complete download: https://www.python.org/ , size of page( 50024 )
    complete download: https://www.python.org/ , size of page( 50024 )
    complete download: https://www.python.org/ , size of page( 50024 )
    complete download: https://www.python.org/ , size of page( 50024 )
    finish, total:0.17750239372253418 sec(s)

    잘 작동하네요. 

    0.17초

     

    동기식

    import time
    from urllib import request
    
    
    def req(url):
        with request.urlopen(url) as response:
            html = response.read()
        return html
    
    
    def download_page(url):
        html = req(url)
        print("complete download:", url, ", size of page(", len(html), ")")
    
    
    def main():
        download_page("https://www.python.org/"),
        download_page("https://www.python.org/"),
        download_page("https://www.python.org/"),
        download_page("https://www.python.org/"),
        download_page("https://www.python.org/")
    
    
    print('start')
    start_time = time.time()
    main()
    print(f'finish, total:{time.time() - start_time} sec(s)')
    start
    complete download: https://www.python.org/ , size of page( 50024 )
    complete download: https://www.python.org/ , size of page( 50024 )
    complete download: https://www.python.org/ , size of page( 50024 )
    complete download: https://www.python.org/ , size of page( 50024 )
    complete download: https://www.python.org/ , size of page( 50024 )
    finish, total:0.33704614639282227 sec(s)

    동기식은 확실히 느리죠? 0.33초

     

    참고) asyncio.run 예전 문법

    # 파이썬 3.7 이후에 포함된 문법
    asyncio.run(main()) 
    
    # 파이썬 3.6까지
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
    loop.close()

    asyncio.get_event_loop()는 파이썬 3.10에서 경고가 발생합니다. 
    get_event_loop 대신 new_event_loop을 사용하면 됩니다. 

    파이썬 공홈에 가보니 버전 3.10부터 get_event_loop는 폐지라고 되어 있네요. 
    실행 중인 이벤트 루프가 없다면 디프리케이션 경고가 발생합니다. 
    미래의 파이썬 릴리즈에서 get_event_loop
    get_running_loop의 alias(가명)가 될 것입니다. 

    Deprecation warning is emitted if there is no running event loop. 
    In future Python releases, this function will be an alias of get_running_loop().

    예전 문법을 이용해서 이렇게 작성해도 될 것 같아서...

    import time
    from urllib import request
    import asyncio
    
    
    def req(url):
        with request.urlopen(url) as response:
            html = response.read()
        return html
    
    
    async def download_page(url):
        # print(len(asyncio.all_tasks()))
        html = await loop.run_in_executor(None, req, url)
        print("complete download:", url, ", size of page(", len(html), ")")
        # print(len(asyncio.all_tasks()))
    
    
    async def main():
        await asyncio.gather(
            download_page("https://www.python.org/"),
            download_page("https://www.python.org/"),
            download_page("https://www.python.org/"),
            download_page("https://www.python.org/"),
            download_page("https://www.python.org/"),
        )
    
    
    print('start')
    start_time = time.time()
    # asyncio.run(main())
    # loop = asyncio.get_event_loop()
    loop = asyncio.new_event_loop()
    loop.run_until_complete(main())
    loop.close()
    print(f'finish, total:{time.time() - start_time} sec(s)')

    비동기로 잘 작동합니다. 

     

    asyncio.create_task()

    코루틴을 Task로 감싸고 실행을 '예약'합니다. Task 객체를 반환합니다.
    라고 공식 홈페이지에 되어 있네요. 

    (제가 비동기 프로그램을 많이 해보지 못해) 이 부분은 좀 모호합니다.
    비동기가 안될 것 같은 느낌인데 비동기가 되네요.
    이렇게 예약을 해두면 게더로 모을 필요가 없는 것 같습니다.

     

    import time
    from urllib import request
    import asyncio
    
    
    def req(url):
        with request.urlopen(url) as response:
            html = response.read()
        return html
    
    
    async def download_page(url):
        # loop = asyncio.get_event_loop()
        loop = asyncio.get_running_loop()
        html = await loop.run_in_executor(None, req, url)
        print("complete download:", url, ", size of page(", len(html), ")")
    
    
    async def main():
        task1 = asyncio.create_task(download_page("https://www.python.org/"))
        task2 = asyncio.create_task(download_page("https://www.python.org/"))
        task3 = asyncio.create_task(download_page("https://www.python.org/"))
        task4 = asyncio.create_task(download_page("https://www.python.org/"))
        task5 = asyncio.create_task(download_page("https://www.python.org/"))
        # print(asyncio.all_tasks())
        await task1
        await task2
        await task3
        await task4
        await task5
        # print(asyncio.all_tasks())
    
    
    print('start')
    start_time = time.time()
    asyncio.run(main())
    print(f'finish, total:{time.time() - start_time} sec(s)')

    이렇게 작성하면, 위에서 보았던 디프리케이션 경고가 발생하지 않습니다. 
    파이썬 3.10부터는 asyncio.run()으로 새 이벤트 루프를 선언하고,
    get_running_loop() [=get_event_loop()]로
    실행 중인 이벤트 루프를 가져온다는 느낌으로 코드를 작성할 수 있을 것 같습니다. 

    이 게시물 작성하면서 처음 한 일이 루프 선언(get_event_loop())을 하나로 모으는 것이었습니다.
    실제로 작동되는 루프는 1개인데, 루프 선언이 여러 번 반복되는 점이 보기 불편했습니다. 
    all_tasks()로 확인하기 전까지는 선언할 때마다 새로운 루프가 생성되나 라는 생각도 잠시 했었습니다.
    파이썬답게 모호한 부분들이 버전 업되면서 명확하게 개선되는군요.
    create_task는 모호한 것 같은데.... ㅜㅜ

    start
    complete download: https://www.python.org/ , size of page( 50108 )
    complete download: https://www.python.org/ , size of page( 50108 )
    complete download: https://www.python.org/ , size of page( 50108 )
    complete download: https://www.python.org/ , size of page( 50108 )
    complete download: https://www.python.org/ , size of page( 50108 )
    finish, total:0.17334628105163574 sec(s)

     

    take1~5까지의 변수로 Task 객체를 받아주지 않고
    바로 await를 걸어 보았습니다.
    동기적으로 작동하네요. 

    이 부분은 파이썬 답지 않게 조금 모호한 느낌입니다. 

    import time
    from urllib import request
    import asyncio
    
    
    def req(url):
        with request.urlopen(url) as response:
            html = response.read()
        return html
    
    
    async def download_page(url):
        loop = asyncio.get_event_loop()
        html = await loop.run_in_executor(None, req, url)
        print("complete download:", url, ", size of page(", len(html), ")")
    
    
    async def main():
        await asyncio.create_task(download_page("https://www.python.org/"))
        # print(asyncio.all_tasks())
        await asyncio.create_task(download_page("https://www.python.org/"))
        # print(asyncio.all_tasks())
        await asyncio.create_task(download_page("https://www.python.org/"))
        # print(asyncio.all_tasks())
        await asyncio.create_task(download_page("https://www.python.org/"))
        # print(asyncio.all_tasks())
        await asyncio.create_task(download_page("https://www.python.org/"))
        # print(asyncio.all_tasks())
    
    print('start')
    start_time = time.time()
    asyncio.run(main())
    print(f'finish, total:{time.time() - start_time} sec(s)')
    start
    complete download: https://www.python.org/ , size of page( 50108 )
    complete download: https://www.python.org/ , size of page( 50108 )
    complete download: https://www.python.org/ , size of page( 50108 )
    complete download: https://www.python.org/ , size of page( 50108 )
    complete download: https://www.python.org/ , size of page( 50108 )
    finish, total:0.3019280433654785 sec(s)

     

    이렇게 해매던 중 이 글에서 힌트를 얻었습니다. 

    asyncio.create_task는 왜 쓰는 걸까?

    이 함수의 역할은, 파라미터로 들어오는 코루틴을 Eventloop에 등록하고 코루틴이 끝났을 때 결과를 받아볼 수 있는 Future 객체를 반환합니다. 반환되는 Future 또한 Awaitable 객체이기 때문에 await을 앞에 붙이면 Eventloop에 실행권을 넘기면서 코루틴의 종료까지 기다릴 수 있지만, 지금은 필요하지 않기 때문에 의도적으로 await 없이 넘어갔습니다. 그 때문에 실행권을 뺏기지 않은 채로 다음 while iteration을 위해 10초간 기다릴 수 있습니다.

    사실 asyncio.create_task()가 반환하는 것은 Task 객체이고, 이 객체는 Future를 상속받기 때문에 동일하게 Awaitable합니다. 

    https://tech.buzzvil.com/blog/asyncio-no-1-coroutine-and-eventloop/?fbclid=IwAR1Gk8tgbMpjJPQIwnjt-rmhH5W_JhIaiEq3DVuds_z1LQf1oyku3Ffz3AE 

     

    asyncio 뽀개기 1 - Coroutine과 Eventloop

    이 시리즈의 목적은 asyncio의 컴포넌트들과 활용법을 소개하는 것입니다. 최종적으로는 실제 production에 쓰이고 있는 graceful shutdown을 구현하는 것을 목표로 하며, 그 과정에서 필요한 asyncio 지식

    tech.buzzvil.com

     

    반응형
Designed by Tistory.