프로젝트+스터디

파이썬 병렬처리(@BACKGROUND)

코낄2 2024. 9. 25. 17:19

모델 알고리즘을 짜다 보면 한 프로세스가 동작하는 중간에 다른 프로세스를 병렬로 동시에 처리해야 할 때가 있다. 예를 들어, 실시간 영상에서 프레임을 처리하다가 중간에 OCR, I/O 등 시간이 소요되는 작업을 해야 할 때 가 있다. 이러한 작업을 동기적으로 처리하면 전체 프로세스가 지연될 수 있기 때문에, 적절한 병렬 처리 기법을 사용하여 효율적으로 처리하는 것이 중요하다.

파이썬은 기본적으로 동기적으로 동작하는 프로그래밍 언어이며, 이는 한 번에 하나의 작업만 처리하는 구조를 의미한다. 파이썬의 전역 인터프리터 락(GIL, Global Interpreter Lock) 때문에 진정한 병렬 처리를 구현하는 데 어려움이 있다. GIL은 여러 스레드가 동시에 파이썬 객체에 접근하는 것을 방지하기 위해 존재하는 일종의 상호 배제 장치이다. 파이썬에서 병렬 처리를 구현할 때 다양한 어려움을 겪었다.

 

파이썬의 병렬 처리 기법

파이썬에서 병렬 처리 기법은 크게 두 가지로 나눌 수 있다: 동시성(Concurrency)병렬성(Parallelism). 이 둘의 차이를 이해하는 것이 중요하다.

  1. 동시성(Concurrency): 여러 작업이 마치 동시에 진행되는 것처럼 보이지만, 사실은 하나의 스레드에서 작업들이 번갈아가며 실행되는 개념이다. CPU에서 여러 작업을 빠르게 전환하여 실행하므로 사용자가 보기에는 동시에 진행되는 것처럼 느껴진다.
  2. 병렬성(Parallelism): 여러 작업이 실제로 동시에 실행되는 개념이다. 여러 개의 CPU 코어를 사용하여 각 작업이 독립적으로 실행된다. 즉, 여러 프로세스나 스레드를 활용하여 진정한 의미의 동시 처리를 구현할 수 있다.

asyncio.create_task  /  background.add_task

asyncio.create_task

asyncio는 파이썬에서 동시성을 처리할 때 주로 사용되는 라이브러리이다. asyncio.create_task는 비동기 함수를 백그라운드에서 실행하기 위해 사용하는 기능이다. 비동기 함수(예: async def func())를 호출하여 await를 사용하는 방식으로 다른 비동기 작업들과 병렬적으로 실행할 수 있다.

import asyncio

async def foo():
    await asyncio.sleep(1)
    print("foo completed")

async def bar():
    await asyncio.sleep(2)
    print("bar completed")

async def main():
    task1 = asyncio.create_task(foo())
    task2 = asyncio.create_task(bar())
    await task1
    await task2

asyncio.run(main())

 

위 코드에서 task1과 task2는 asyncio.create_task를 통해 생성되며, foo()와 bar() 함수는 main() 함수에서 동시에 실행되는 것처럼 보인다. 하지만 실제로는 하나의 스레드에서 작업들이 번갈아가며 실행된다. 따라서 asyncio는 진정한 의미의 병렬 처리를 구현할 수는 없으며, 단일 스레드에서 비동기적으로 여러 작업을 전환하여 실행할 수 있을 뿐이다.

background.add_task

FastAPI에서 제공하는 background.add_task는 백그라운드 작업을 실행하기 위해 사용할 수 있는 또 다른 기법이다. 웹 서버에서 시간이 오래 걸리는 작업을 비동기적으로 실행하여 서버의 응답 시간을 단축시키는 데 사용된다.

from fastapi import FastAPI, BackgroundTasks

app = FastAPI()

def write_log(message: str):
    with open("log.txt", "a") as log_file:
        log_file.write(message + "\n")

@app.post("/log/")
async def log_message(message: str, background_tasks: BackgroundTasks):
    background_tasks.add_task(write_log, message)
    return {"message": "Log entry created"}

위 예제에서 write_log 함수는 background_tasks.add_task를 통해 백그라운드에서 실행된다. 클라이언트는 즉시 "Log entry created"라는 응답을 받을 수 있으며, 서버는 비동기적으로 write_log 함수를 실행한다. 하지만 background.add_task도 asyncio 기반이므로, 진정한 병렬 처리가 아닌 동시성을 제공할 뿐이다.

 

백그라운드 스레드 데코레이터 사용

병렬 처리를 구현하기 위해 파이썬의 threading 모듈을 사용할 수 있다. 다음과 같은 간단한 데코레이터를 정의하여, 특정 함수가 별도의 스레드에서 실행되도록 할 수 있다.

import threading

def BACKGROUND(func):
    '''
    백그라운드 스레드 데코레이터
    @BACKGROUND 표기를 함수 정의부 윗줄에 지정한다.   
    '''    
    def backgrnd_func(*a, **kw):
        threading.Thread(target=func, args=a, kwargs=kw).start()
    return backgrnd_func

 

위의 BACKGROUND 데코레이터는 주어진 함수를 새로운 스레드에서 실행하도록 한다. 예를 들어, 특정 함수가 시간이 오래 걸리는 작업을 수행해야 할 때, 이를 @BACKGROUND로 지정하여 백그라운드에서 실행되도록 할 수 있다.

@BACKGROUND
def long_running_task():
    for i in range(5):
        print(f"Task running... {i}")
        time.sleep(1)

long_running_task()
print("Main thread continues to run")

위 예제에서 long_running_task() 함수는 새로운 스레드에서 실행되므로, 메인 스레드는 해당 함수가 완료될 때까지 대기하지 않고 계속해서 실행된다. 이를 통해 I/O 바운드 및 CPU 바운드 작업을 동시에 처리할 수 있다.

 

백그라운드 스레드 데코레이터의 장점

  1. 간결함: 코드의 간결성을 유지하면서도, 백그라운드 스레드에서 작업을 처리할 수 있다.
  2. 병렬성: GIL의 영향을 받지만, I/O 바운드 작업이나 파이썬 외부의 C/C++ 라이브러리를 사용할 경우 병렬 처리가 가능하다.
  3. 유연성: threading 모듈을 사용하기 때문에, 필요에 따라 스레드 풀을 구성하거나 스레드 동기화 작업도 쉽게 구현할 수 있다.

결론

파이썬 서버에서 비동기 작업을 진행하는 다양한 방법을 잘 익혀두는 것이 좋다. asyncio나 background.add_task와 같은 동시성 기법을 이해하고, threading이나 multiprocessing을 사용한 병렬 처리 기법도 필요에 따라 적절히 활용해야 한다. 작업의 종류와 환경에 맞는 적절한 병렬 처리 기법을 선택하여, 효율적이고 정확한 알고리즘을 작성하는 개발자가 되자!