안녕하세요. 휴먼스케이프의 개발자 bruno입니다.
이 번 포스트에서는 python에서 동시성을 처리하는 기능 중 하나인, Future 클래스에 관해 다루겠습니다.
Future는 concurrent.futures와 asyncio 내부에 있는 핵심 컴포넌트입니다.
사용자가 직접 다룰 일은 거의 없지만, 지연된 계산을 표현하기 위해 사용됩니다. 이 때, 그 객체의 계산은 완료되었을 수도 있고, 완료되지 않았을 수도 있습니다.
Future는 대기 중인 작업을 큐에 넣고, 완료 상태를 조사하고, 결과 혹은 예외를 가져올 수 있도록 캡슐화합니다.
주의할 점은, Future의 실행을 스케줄링하는 프레임워크만이 어떤 일이 일어나는지 확실히 알 수 있기 때문에, 사용자가 Future 객체를 직접 생성하거나 변경해서는 안된다는 것입니다. 이 주의사항을 무시한다면 큰 고통에 빠질 수 있습니다.
concurrent.futures를 이용한 멀티스레딩
다음은 concurrent.futures를 이용해서 여러 스레드로 각 나라의 국기 이미지를 내려받는 코드의 일부입니다.
executor.map이 아닌 executor.submit()과 executor.as_completed()를 사용해서 완료 전후의 Future 객체를 확인할 수도 있습니다. 다음은 그 예제입니다.
Executor.map()을 사용할 때와 Executor.submit(), Executor.as_completed()를 사용할 때의 또 다른 큰 차이가 있습니다.
Executor.map()의 경우, 호출한 순서 그대로 결과를 반환합니다. 그와 다르게 Executor.submit(), Executor.as_completed()를 함께 사용하면, 완료되는 순서대로 결과를 가져오게 됩니다.
블로킹 I/O와 GIL
CPython은 내부적으로 스레드 안전하지 않기 때문에, Global Interpreter Lock(GIL)을 이용합니다. GIL은 한 번에 한 스레드만 python 코드를 실행하도록 제한합니다.
GIL — 위키백과
CPython에서의 GIL은 Python 코드(bytecode)를 실행할 때에 여러 thread를 사용할 경우, 단 하나의 thread만이 Python object에 접근할 수 있도록 제한하는 mutex 이다. 그리고 이 lock이 필요한 이유는 CPython이 메모리를 관리하는 방법이 thread-safe하지 않기 때문이다.스레드 안전(thread safety)
멀티 스레드 프로그래밍에서 일반적으로 어떤 함수나 변수, 혹은 객체가 여러 스레드로부터 동시에 접근이 이루어져도 프로그램의 실행에 문제가 없음을 뜻한다. 보다 엄밀하게는 하나의 함수가 한 스레드로부터 호출되어 실행 중일 때, 다른 스레드가 그 함수를 호출하여 동시에 함께 실행되더라도 각 스레드에서의 함수의 수행 결과가 올바로 나오는 것으로 정의한다.
때문에 블로킹 입출력을 실행하는 모든 표준 라이브러리 함수는 GIL을 우회하는 방식을 사용합니다. python 스레드가 네트워크로부터의 응답을 기다리는 동안, 블로킹된 입출력 함수가 GIL을 해제함으로써 다른 스레드가 실행될 수 있도록 합니다.
이런 이유로 데이비드 비즐리는 ‘파이썬 스레드는 아주 능숙하게 게으름을 피운다'고 했다고 합니다.
concurrent.futures로 프로세스 실행하기
concurrent.futures엔 멀티스레딩을 가능하게 해주는 ThreadPoolExecutor 뿐만 아니라 멀티프로세싱을 가능하게 해주는 ProcessPoolExecutor가 있습니다.
ProcessPoolExecutor는 작업을 여러 파이썬 프로세스에 분산시켜 진정한 병렬 컴퓨팅을 가능하게 합니다. ProcessPoolExecutor는 GIL을 우회하므로 계산 위주의 작업을 수행해야 하는 경우 가용한 CPU를 모두 사용합니다.
ThreadPoolExecutor와 ProcessPoolExecutor 모두, 범용 Executor 인터페이스를 구현하므로, concurrent.futures를 사용하는 경우에는 스레드 기반의 프로그램을 프로세스 기반의 프로그램으로 쉽게 변환할 수 있습니다.
아래는 ProcessPoolExecutor를 사용한 예제입니다. ThreadPoolExecutor와 매우 유사함을 보실 수 있습니다.
ProcessPoolExecutor는 os.cpu_count()가 반환하는 값을 이용해서 프로세스 수를 정하므로, 대부분의 경우 직접 설정할 필요가 없습니다.
스레드 및 멀티프로세싱의 대안
concurrent.furtures는 후에 추가된 방법으로, 파이썬은 0.9.8버전(1993년)부터 스레드를 지원했습니다. 현재 python3에서는 원래의 thread 모듈의 사용 중단을 안내했으며, 더 높은 수준의 threading 모듈을 사용할 것을 권장하고 있다고 합니다.
futures.ThreadPoolExecutor로 처리하기 어려운 작업을 수행하는 경우엔 Thread, Lock, Semaphore 등 threading 모듈의 기본 컴포넌트를 이용하여 처리할 수 있습니다. 스레드 간에 데이터를 전송할 때는 queue 모듈에서 제공하는 thread safty(스레드 안전)한 큐를 사용할 수 있습니다. 이 컴포넌트들은 futures.ThreadPoolExecutor에 캡슐화되어 있습니다.
멀티프로세싱의 경우, 대부분은 futures.ProcessPoolExecutor로 처리할 수 있지만, 애플리케이션의 구조가 이 클래스에 잘 맞지 않는 경우에는 threading API와 비슷하지만 프로세스에 작업을 할당하는 multiprocessing 패키지를 사용해야 합니다.
감사합니다.
이 포스트는 루시아누 하말류의 책 [Fluent Python]을 참고해서 작성했습니다.
Get to know us better!
Join our official channels below.
Telegram(EN) : t.me/Humanscape
KakaoTalk(KR) : open.kakao.com/o/gqbUQEM
Website : humanscape.io
Medium : medium.com/humanscape-ico
Facebook : www.facebook.com/humanscape
Twitter : twitter.com/Humanscape_io
Reddit : https://www.reddit.com/r/Humanscape_official
Bitcointalk announcement : https://bit.ly/2rVsP4T
Email : support@humanscape.io