비동기 최적화 및 멀티스레딩async&await/asyncio/concurrnt.futures

Ruah·2024년 9월 25일

Python에서 비동기 및 병렬 처리를 최적화하기 위해 사용되는 여러 라이브러리 및 메커니즘은 I/O 작업이나 CPU 작업의 병렬화를 통해 코드 실행 속도를 높일 수 있다.

1. 비동기 처리의 기본 개념

1-1. async와 await

  • async : 비동기 함수로(코루틴)을 정의할 때 사용한다. 일반함수와 다르게 비동기 함수는 호출과 동시에 실행되지 않으며, 다른 작업과 동시에 처리될 수 있다.
  • await : 비동기 함수에서 사용되며, 비동기 작업이 완료될 때까지 기다리도록 한다. await 뒤에 오는 비동기 함수의 실행이 완료되면 그 다음 코드가 실행된다.
    사용예시:
async def async_func():
	await asyncio.sleep(1)
    return "비동기 작업 완료"

async def main():
	result = await async_func()
    print(result)

asyncio.run(main())

: 이 코드는 1초동안 비동기적으로 대기한 후 작업을 완료한다.

1-2. asyncio

  • asyncio는 Python의 비동기 처리 라이브러리로, 이벤트 루프(event loop)를 기반으로 비동기함수 (코루틴)을 실행한다.
  • asyncio.run을 사용해 비동기 함수의 실행을 시작 가능.
import asyncio

async def async_func():
	await asyncio.sleep(1)
    return "비동기 작업 완료"

async def main():
    results = await asyncio.gather(async_func(), async_func())
    print(results)

asyncio.run(main())

: async_func을 동시에 두번 호출하여, 각 작업이 완료될 때까지 기다린다.

2. 동시성 처리 라이브러리

2-1. concurrent.futures

  • concurrent.futures는 동시성 처리를 위한 고수준의 인터페이스를 제공하며, 쓰레드 풀(ThreadPoolExecutor)과 프로세스 풀(ProcessPoolExecutor)을 통해 병렬 작업을 처리한다.

ThreadPoolExecutor

  • ThreadPoolExecutor는 여러 작업을 동시에 싫애할 때 사용된다. CPU를 많이 사용하지 않는 I/O-bound 작업에서 유용하다.
import concurrent.futures

def sync_task(seconds):
    time.sleep(seconds)
    return f"{seconds}초 후 작업 완료"

with concurrent.futures.ThreadPoolExecutor() as executor:
    futures = [executor.submit(sync_task, sec) for sec in [1, 2, 3]]
    results = [f.result() for f in concurrent.futures.as_completed(futures)]
    print(results)

: 1초, 2초, 3초 동안 대기하는 작업을 동시에 실행하고 결과를 반환

ThreadPoolExecutor & asyncio.to_thread

  • ThreadPoolExecutor는 여러개의 스레드풀을 생성하고 각 스레드에서 작업을 병렬로 처리할 수 있도록 한다.
  • I/O 작업을 병렬로 처리할 때 적합하며, 비동기 함수 안에서 await asyncio.to_thread를 사용해 비동기 처리와 통합이 가능하다.
import asyncio
import concurrent.futures

def sync_task(seconds):
    time.sleep(seconds)
    return f"{seconds}초 후 작업 완료"

async def async_main():
	with concurrent.futures.ThreadPoolExecutor() as executor:
    	loop = asyncio.get_event_loop()
        tasks = [loop.run_in_executor(executor, sync_task, sec) for sec in [1,2,3]]
        result = await asyncio.gather(*tasks)
        print(results)
asyncio.run(async_main())

: 이 코드는 비동기 작업과 스레드 풀을 함께 사용하여 동시적으로 실행된다.

3. 최적화 기법

3-1. asyncio.to_thread

  • asyncio.to_thread
    : 기존의 동기 함수를 비동기로 호출할 때 사용.
    : CPU-bound 작업이 아닌, 주로 파일을 입출력, 네트워크 통신등의 I/O-bound 작업에 사용하면 효율적
    : 내부적으로는 ThreadPoolExecutor를 사용하여 비동기 작업을 처리
    import asyncio, time
    
    def blocking_task():
    	time.sleep(2)
    	return "블로킹작업 완료"
    async def main():
    	result = await asyncio.to_thread(blocking_task)
        print(result)
    
    asyncio.run(main())
    : 이 코드는 blocking_task를 비동기적으로 호출하여 블로킹 없이 2초 대기 후 작업을 완료한다.

3-2. asyncio.get_event_loop

  • get_event_loop
    : 실행중인 이벤트 루프를 가져오는 함수
    : 이를통해 비동기 작업을 직접 실행하거나, 기존 비동기 작업에 새로운 작업 추가 가능
    import asyncio
    
    async def async_task():
    	await asyncio.sleep(1)
        pirnt("비동기 작업 완료:)
    
    loop = asyncio.get_event_loop()
    loop.run_until_complete(async_task())
    : 이코드는 이벤트 루프에서 비동기 작업을 실행하는 방법을 보여준다.

3-3. asyncio.run_in_executor

  • run_in_executor
    : 기존 동기함수를 비동기적으로 실행할 수 있도록 함.
    : 이는 ThreadPoolExecutor와 함께 사용가능하며, CPU-bound 작업을 비동기 환경에서 실행하는데 유용하다.

     import asyncio
    import time
    
    def blocking_task(seconds):
        time.sleep(seconds)
        return f"{seconds}초 후 작업 완료"
    
    async def main():
        loop = asyncio.get_event_loop()
        result = await loop.run_in_executor(None, blocking_task, 2)
        print(result)
    
    asyncio.run(main())

    : 블로킹 작업을 비동기적으로 처리하면서도, 비동기 함수에서 동기 작업을 실행하는 코드

비동기 작업관리

4-1. asyncio.gather

  • asyncio.gather
    : 여러 비동기 작업들 동시에 실행하고 결과를 모음
    : 여러작업을 병렬로 처리할 때 유용하며, 작업이 모두 완료 될때까지 대기
    import asyncio
    
    async def task():
    	await asyncio.sleep(1)
        return "작업1완료"
    async def task2():
    	await asyncio.sleep(2)
        return "작업2완료"
    
    async def main():
    	results = await asyncio.gather(task1(), task2())
        print(results)
        
    asyncio.run(main())
    : 두개의 작업을 동시에 실행하고 작업이 모두 완료될때까지 기다리는 코드
profile
집요한 주니어 개발자의 호되게 당했던 기록

1개의 댓글

comment-user-thumbnail
2024년 9월 25일

멋있네요 잘봤어요

답글 달기