언어

[Python] 동시처리/병렬처리 방법

마디니 2022. 3. 2. 21:48
반응형

순차처리/동시처리/병렬처리

  • 순차 처리는 하나의 작업을 순차적으로 실행 하는 것. 싱글 코어(single-core), 싱글 스레드(single-thread)에서 처리를 수행하는 상황이 이에 해당되며 파이썬에서는 의도적으로 동시처리를 구현하지 않는 한 항상 순차처리를 수행한다.
  • 동시 처리는 한 사람이 여러개의 작업을 동시에 처리해 나가는 상황이며 파이썬 프로그램에서는 다중 스레드를 사용하는 상황이 이해 해당된다.
  • 병럴 처리는 여러명이 여러개의 작업을 동시에 처리하는 형태를 말하며 파이썬 프로그램에서는 멀티코어로 다중 프로세스를 사용하는 상황
  • 모든 병렬 처리는 동시처리이기는 하나, 동시 처리라고 해서 모두가 병렬 처리인 것은 아니다.

파이썬과 동시 처리

  • 파이썬에서 동시 처리는 다중 스레드나 다중 프로세스를 사용해 구현하거나 이벤트 루프를 사용한다.
  • 일반적으로 실행 시 오버헤드가 큰 순서대로 나열하자면  다중 프로세스, 다중 스레드, 이벤트 루프순이다.
  • 어떤 방법이 적합한지는 CPU바운드 처리인지 I/O바운드 처리인지에 따라 결정된다.
    • CPU 바운드 처리
      • 암호화(복호화), 숫자계산등 cpu자원을 사용해 계싼을 수행하는 처리로 여러 코어를 동시에 사용해 병렬 처리를 할 수 있는 다중 프로세스가 좋다.
      • 파이썬에는 GIL이 있으므로 다중스레드, 이벤트 루프를 통한 처리 고속화는 기대할 수 없다
    • I/O바운드 처리
      • 데이터베이스 접속이나, web api이용등 통신에 의한 대기시간이 발생하는 처리
      • 아중 프로세스, 다중 스레드, 이벤트 루프 모두 유용
GIL(Global Interpreter Lock)
파이썬 인터프리터 전체에서 공유하는 잠금기능으로 여러 스레드가 있을 때, GIL을 얻는 하나의 스레드만 파이썬의 바이트 코드를 실행 할 수 있도록 설계되어 있다. 이런 구조적인 특징으로 다중 스레드를 이용한 동시 처리에서는 cpu 바운드 처리에서 빠른 속도를 기대할 수 없다.

 

concurrent.futures 모듈 

concurrent.futures모듈은 동시처리를 위한 표준 라이브러리이다. 동시처리에서 실행할 처리를 전달하면 해당 처리를 future객체에 캡슐화해서 비동기 처리로 실행해 준다.

concurrent.future 모듈에서는 비동기로 수행하고 싶은 처리를 호출 가능 객체로 취급한다. 이 호출 가능 객체를 Executor클래스의 메서드 submit()에 전달하면 그 처리 실행을 스케줄링한 Future 클래스 인스턴스를 반환한다.

Executor의 구현체인 ThreadPoolExecutor 클래스나 ProcessPoolExecutor 클래스를 이용한다.

 

from concurren.futures import (
    ThreadPoolExecutor,
    Future
)

# 비동기로 수행할 처리
def func(): 
    return 1
    
# 비동기로 수행할 처리를 submit()에 전달
future = ThreadPoolExecutor().submit(func)

>>> isinstance(future, Future)
True


# 비동기로 실행한 처리의 반환값을 취득
>>> future.result()
1

# 현재 상태를 확인
>>> future.done()
True
>>> future.running()
False
>>> future.cancelled()
False

언제 어떤 스레드나 프로세스로 처리할 것인지 스케줄링 하는것은  future의 역할이고, 사용자는 Executor 클래스의 메서드 submit()을 호출해, 처리를 비동기 실행으로 스케줄링 하도록 요청하는 작업만 할 수 있다.

 

ThreadPoolExecutor 클래스 - 스레드 기반 비동기 실행

  • I/O바운드한 처리에서 다중 스레드가 효과적이다.
  • 여러 스레드가 동시에 같은 객체에 접근하면 예상대로 동작하지 않을 수 있는데 스레드 세이프한 구현으로 이를 방어할 수 있다.
  • 스레드 세이프한 구현 : threading.Lock 객체를 이용해서 락을 통한 배타제어를 삽입 한다. 록을 얻는 스레드만 처리를 실행할 수 있고, 특정 스레드라 락을 취하면 해당 락이 해제될 때까지 다른 스레드는 락을 얻을 수 없는 단순한 구조이다.
import threading
class ThreadSafeFunc:
	#락 준비
    lock = threading.Lock()
    def __init__(self):
        self.count = 0
    def increment(self):
        with self.lock:
            #배타 제어할 처리를 이안에 구현
            self.count = self.count+1

 

ProcessPoolExecutor 클래스 - 프로세스 기반 비동기 실행

  • I/O뿐 아니라 Cpu바운드한 처리의 고속화에도 유용하다. (다중 프로세스에서는 GIL의 제약을 받지 않고 여러 코어를 동시에 사용하기 때문)
  • 구현방법은 스레드 기반과 같다.
  • ProcessPoolExecutor를 사용할 때 주의점
    • 클래스는 큐라고 불리는 데이터 구조를 사용해 프로세스 사이에서 객체 전달을 수행한다. 그리고 이 큐에 추가된 객체는 pickle이라고 불리는 형식으로 직렬화 되기 때문에, ProcessPoolExecutor 클래스를 사용한 다중 프로세스 처리에서는  pickle화 가능한 객체만 실행, 반환할 수 있다.
    •  lambda나 모듈 최상위 레벨 이외에서 정의된 함수나 클래스들은  pickle 화 할 수 없다.
    • 난수 사용시 numply.random.seed()함수를 사용했을 때 프로세스 시작 방식이 fork로 설정되어 있는 환경에서 실행시 같은 숫자값이 발생한다. -> random.random() 사용으로 해결 가능

* fork는 부모 프로세스를 복제해서 자녀 프로세스를 만드는 방식이다.

asyncio 모듈 - 이벤트 루프를 사용한 동시 처리 수행

  • asyncio는 이벤트 루프를 사용해 동시 처리를 가능하게 해주는 모듈이다. 이 방법을 사용하면 단일 스레드에서도 동시처리가 가능하다.
  • asyncio모듈을 사용하려면 반드시 코루틴이 필요하다

코루틴

  • 코루틴(coroutine)은 서브 루틴과 같이 일련의 처리를 모아둔 것인데, 서브루틴은 파이썬에서는 함수에 해당하고, 한번 호출되면 앞에서부터 마지막까지 한번에 실행한다. 이 특성을 이용해 여러 처리가 동시에 이루어지도록 구현한다.
  • 파이썬에서 함수 정의시 def 대신 async def 를 사용하여 코루틴을 반환 할 수 있다.
  • await문을 사용해 코루틴을 호출하거나 중단할 수 있다. 
  • asyncio.gather()은 여러 코루틴을 받아 각각의 실행을 스케줄링한다.
  • 코루틴을 동작시키기 위해서는 이벤트 루프와 태스크가 필요하다.
  • 코루틴은 실행이 스케줄링 되면 태스크가 된다.
  • 이벤트 루프가 I/O이벤트에 맞춰 태스크 실행을 제어 한다. 
#코루틴 정의 예시
async def coro():
	return 1
    
#반환값은 1이 아닌 코루틴 객체
>>> coro()
<coroutine object coro at 0cxwk3ks9>

#코루틴의 실행
import asyncio

>>> asyncio.run(coro())
# 서로 다른 코루틴을 호출할 때 await키워드를 사용한다. 
# await 키워드가 있어도 일반적인 함수호출과 같은 형대로 반환값을 얻을 수 있다.
# await 키워드가 없으면 코루틴 객체만 생성될 뿐 실행은 되지 않는다.

import asyncio
import random
async def call_sleep(url):
	await asyncios.sleep(random.random())
    print(f'got a response: {url}')
    return url

async def async_download(url):
	#await를 사용해 코루틴을 호출
    response = await call_sleep(url)
    return response

코루틴의 실행방법

  1. asyncio.run()에 전달하는 방법
  2. 코루틴 내부에서 await 코루틴으로 실행하는 방법
  3. 태스크를 만들어 실행하는 방법
    • 태스크는 스케줄링한 코루틴을 캡슐화 한 것이다.
    • 태스크는 asyncio.Future 클래스의 서브 클래스인  asyncio.Task 클래스의 인스턴스지만, Future클래스와 달리 result()메소드로 결과를 얻지 않고, await키워드로 실행해, 동기처리와 같이 반환값을 얻는다.

 

 

 

 

참고서적) 효율적 개발로 이끄는 파이썬 실천 기술- 스야마레이 지음/김연수 옮김

반응형