Python 프로그래밍

[Python] Future

DayOff 2022. 6. 1. 16:40

 Python의 동시성을 처리하는 기능 중 하나인, Future 클래스에 대해 알아보고자 한다. Future는 concurrent.futures와 asyncio 내부에 있는 핵심 컴포넌트입니다.

사용자가 직접 다룰 일은 거의 없지만, 지연된 계산을 표현하기 위해 사용됩니다. 이 때, 그 객체의 계산은 완료되었을 수도 있고, 완료되지 않았을 수도 있습니다.

Future는 대기 중인 작업을 큐에 넣고, 완료 상태를 조사하고, 결과 혹은 예외를 가져올 수 있도록 캡슐화합니다.

주의할 점은, Future의 실행을 스케줄링하는 프레임워크만이 어떤 일이 일어나는지 확실히 알 수 있기 때문에, 사용자가 Future 객체를 직접 생성하거나 변경해서는 안된다는 것입니다. 이 주의사항을 무시한다면 큰 고통에 빠질 수 있습니다.

 

Future

 concurrent.futures.Future 비동기로 호출된 함수 콜이 객체로 캡슐화된 형태이다. Executor 클래스 인스턴스의 .submit() 호출에 의해 인스턴스가 만들어진다. 특히 이 객체는 asyncio의 Future 클래스와 유사한 API를 가지고 있다. (둘이 호환되는 객체는 아니다.)

 따라서 단일 스레드 비동기 코루틴을 사용하는 방식과 concurrent.futures를 이용한 병렬처리 방식은 매우 비슷한 형태로 사용 가능하다. 이 Future 클래스는 자바스크립트의 Promise API와 매우 비슷하다. 아직 완료되지 않은 (혹은 완료되었는지 당장은 모르는) 작업을 외부에서 객체로 다룰 수 있게 된다.

 다음의 메소드들이 지원된다. 특히 하나의 작업에 대해서 하나 이상의 완료 콜백을 추가할 수 있다는 점이 흥미롭다.

  • cancel() : 작업 취소를 시도한다. 만약 현재 실행중이고 취소가 불가능할 경우 False를 리턴한다. 작업이 취소되었다면 True가 리턴된다.
  • canceled() : 취소가 완료된 작업이면 True를 리턴한다.
  • running(): 실행 중인 경우 True를 리턴한다.
  • done(): 작업이 완료되어고 정상적으로 종료되었다면 True를 리턴한다.
  • result(): 해당 호출의 결과를 리턴한다. 만약 작업이 아직 완료되지 않았다면 최대 타임아웃시간까지 기다린다음, None을 리턴한다.
  • exception(): 해당 호출이 던진 예외를 반환한다. 역시 작업이 완료되지 않았다면 타임아웃 시간까지 기다린다.
  • add_done_callback(): 콜백함수를 Future 객체에 추가한다. 이 함수는 future 객체하나를 인자로 받는 함수이다. 콜백은 취소되거나 종료된 경우에 모두 호출된다.
# flags_threadpool.py
from concurrent import futures
# Reuse functions in flags.py
from flags import save_flag, get_flag, main 

# 하나의 국기 이미지를 다운받는 함수. 각 worker가 이 함수를 수행함
def download_one(cc: str):
	image = get_flag(cc)    
	save_flag(image, f'{cc}.gif')    
	print(cc, end=' ', flush=True)    
	return cc 

def download_many(cc_list: list[str]) -> int:
	# context manager로서 ThreadPoolExecutor를 인스턴스화 합니다.    
	# executor.__exit__() 메소드는 executor.shutdown(wait=True)를 호출하는데,    
	# 이는 스레드가 완료될 때까지 블락시킵니다.   
	with futures.ThreadPoolExecutor() as executor:        
		# map 메소드는 내장 함수 map과 유사합니다.        
		# 첫 번째 인수인 download_one 함수는 여러 스레드에서 동시에 호출됩니다.        
		# map 메소드는 각 함수 호출에서 리턴되는 값들을 반복할 수 있는 제너레이터를 반환합니다.      
		# 여기서는 country code를 반환        
		res = executor.map(download_one, sorted(cc_list))        

	return len(list(res)) 


if __name__ == '__main__':    
	# 이 스크립트에서 구현한 downlaod_many 콜러블을 인수로 전달하여,    
	# flags.py에서 구현한 main 함수 호출   
	main(download_many)