멀티 스레딩을 통한 성능향상, ThreadPoolExecutor() 사용하기

youngh·2024년 8월 19일

인턴일지

목록 보기
2/4
post-thumbnail

이전 포스팅에서 16초가 걸리던 응답 시간을 어떻게 해결해야할 지 고민해보았다. 결론은 멀티 스레딩을 사용하여 여러 작업을 병렬적으로 처리하자!

현재 작업의 순서는 S3에 있는 이미지를 로드하여 퍼즐선을 그린 후 answer 텍스트를 찍고 퍼즐판 배경을 생성하여 퍼즐판에도 퍼즐선을 그린 후 problem 텍스트를 찍는 작업이다. (글로 쓰는 것부터 복잡한 것 같다^^;)

순차적으로 진행을 하다보니 더 오래걸린다는 생각을 하였고 이를 동시에 진행한다면 더 짧은 시간안에 클라이언트에게 response를 전달할 수 있을 것이라 생각했다.

아직 운영체제 수업을 듣지는 못했지만 (다음학기에 들을 계획이다 ㅎ) 멀티 스레딩과 멀티 프로세싱을 통해 병렬적으로 작업을 실행할 수 있다는 점은 알고 있었다. 이에 대한 구체적인 공부가 필요하다고 생각했고 인터넷과 유튜브, 학교 도서관에서 전공 서적들을 한번 찾아보았다. (자세한 내용은 적지 않을 것이다 해당 내용은 인터넷에 널려있기 때문!)

참고한 자료:

우테코 발표 영상 1, 우테코 발표 영상 2
개인적으로 우테코 발표 영상을 꾸준히 보는 것을 강추한다!
지하철에서 보면 10분, 20분 만에 알찬 CS 지식을 금방금방 얻을 수 있다.
다들 발표를 너무 잘하심..
그리고 파이썬 병렬 처리 중 가장 중요한 GIL에 대한 내용도 이 영상을 통해 이해할 수 있었다.(영어 주의)

어쨌든,
먼저, 작업을 ‘병렬적’으로 처리하기 위해서는 어떤 작업들이 있는지 세분화할 필요가 있었다.

그래서 나는 위 순차적 작업을 하나하나 나누었다

  1. S3의 이미지를 로드하기
  2. 이미지에 퍼즐선과 answer 텍스트 그리기
  3. 퍼즐판 (puzzle base) 생성하기
  4. 퍼즐판에 퍼즐 선과 problem 텍스트 그리기
  5. 완성본 docx 파일을 S3에 저장하기

위 5가지 작업중 1과 2는 서로 의존적이기 때문에 병렬적으로 처리가 어려웠다. (이미지가 로드 되어야 퍼즐선과 텍스트를 그릴 수 있기 때문)

3과 4도 동일한 이유로 병렬적 처리가 어려웠다.

따라서 나는 1과 2, 3과 4를 두가지의 큰 작업으로 나누어 이 둘을 병렬적으로 처리하고 마지막으로 5번 작업을 진행시키고자 했다.

수정된 코드 (1번, 2번 작업과 3번, 4번 작업을 나눔):

	# 1과 2 작업을 통합한 함수 (퍼즐 이미지 작업)
    def draw_puzzles_with_answers(self, problem):
        try:
		    #변환할 이미지의 S3 url 지정
            upload_key = f"export/puzzle/{problem.id}.png"
            font_num = 8 #그림에 찍을 글자의 크기 지정
		          
			#S3 이미지를 BytesIO 객체로 변환
			bytes_img_memory = self.save_s3_image_to_memory(self, upload_key)
            draw = ImageDraw.Draw(image)
            
            #퍼즐에 찍을 answer 텍스트 추출
            answers = {
                key: value
                for option in problem.problem_options
                for key, value in option.items()
                if key.startswith("answer_")
            }
            data = answers

			#외부에서 정의한 CreateMathPuzzle 클래스 -> 퍼즐의 선과 전달받은 answer 텍스트를 찍는 역할
		    #퍼즐선과 answer 텍스트를 찍고 해당 퍼즐 완성본을 bytes_img_memory에 업데이트
			#이 메서드는 기존 local에서 처리하던 것을 bytesIO로 처리하게끔 따로 수정
            puzzleDrawer = CreateMathPuzzle()
            puzzleDrawer.completion_puzzle(
                data, draw, bytes_img_memory, font_num
            )
						
			#위에서 업데이트 된 bytes_img_memory의 퍼즐 완성본을 미리 선언해둔 document(python_docx) 객체에 찍기
            self.document.add_picture(bytes_img_memory, width=Inches(6))
            self.document.add_page_break()

			#파이썬은 자동으로 메모리 해제를 해주지만 명시하여 자원 해제 명확화
			bytes_img_memory.close()
            
        except Exception as e:
            logger.error(f"Failed to draw puzzle base: {e}")
            raise e

	#3과 4 작업을 통합한 함수 (퍼즐판 작업)
    def draw_puzzlebases_with_problems(self, problem):
        try:
            #이미지 퍼즐 조각을 맞출 퍼즐판 제작하기
		    #CreateMathPuzzle 인스턴스를 호출해 퍼즐판 제작 메서드 호출 (퍼즐판 배경만 생성)
            base_image_url = puzzleDrawer.create_puzzle_base()
            
		    #퍼즐판 svg파일 url을 png 파일로 변환
            base_image = self.fetch_image(base_image_url)
						
			#퍼즐판도 BytesIO 객체로 생성
			base_img_memory = BytesIO()
            base_image.save(base_img_memory, format="PNG")
            base_img_memory.seek(0)
            draw = ImageDraw.Draw(base_image)
            
            #problem 텍스트 추출
            problems = {
                key: value
                for option in problem.problem_options
                for key, value in option.items()
                if key.startswith("problem_text_")
            }
            data = problems
            font_num = 15
            
			#CreateMathPuzzle 인스턴스 호출 후 퍼즐판 선이랑 problem 텍스트 찍기
            puzzleDrawer.completion_puzzle(
                data, draw, base_img_memory, font_num, True
            )
            
		    #퍼즐판 완성본 docx 객체에 찍기
            self.document.add_picture(base_img_memory, width=Inches(6))

			#메모리 해제 명시
			base_img_memory.close()
            
        except Exception as e:
            logging.error(f"Failed to draw puzzle base: {e}")
            raise e

위 두 함수를 병렬적으로 처리하고 완성본을 S3에 저장하는 함수:

#완성본을 S3에 저장하는 함수
def generate_s3_problem_file(self, problems, subject):
        try:
		    #파이썬의 멀티 스레딩 모듈 사용
            with concurrent.futures.ThreadPoolExecutor() as executor:
		        #각 병렬 처리된 작업의 결과를 저장하는 객체
                futures = []
                
                #매개변수로 전달받은 쿼리를 돌며 반복 (problems는 쿼리 형태)
                for problem in problems:
		            #각 병렬 작업을 futures 객체에 추가
                    futures.append(
                        executor.submit(self.draw_puzzles_with_answers, problem)
                    )
                    futures.append(
                        executor.submit(self.draw_puzzlebases_with_problems, problem)
                    )
				#Future 객체들을 확인하면서 오류 발생시 예외를 re-raise 함으로써 예외 처리
                for future in concurrent.futures.as_completed(futures):
                    future.result()
                    
        except Exception as e:
            logging.error(f"Export Failed: {e}")
            executor.shutdown(wait=False)
            return "Export Failed"

결과는..?

11초..! 나름 1/3을 줄일 수 있었다.

어떤식으로 동작하는 지 궁금하여 세분화한 작업이 걸리는 시간을 로그를 찍어서 확인해보았다.

위에서 볼 수 있듯이 ‘퍼즐 이미지 그리기’ 작업과 ‘퍼즐판 그리기’ 작업이 동시에 (병렬적으로) 실행되었다. 그 후 더 오래 걸리는 작업 시간만큼만 (퍼즐 이미지 작업) 걸린 후 완성본을 저장하였다.

만약 병렬처리가 안되었다면 5.1초 + 5.6초 + 5.8초로 앞 포스팅 처럼 16초 정도가 걸렸을 것이다. 하지만 병렬적으로 처리함으로써 더 짧은 시간의 작업 시간을 상쇄시킬 수 있었다..!

33초가 걸리던 응답 속도를 10~11초로 줄이다니..! 별거 아니었던 것 같지만 약 66프로의 성능 향상이 이루어져 나름 뿌듯하고 감격스러웠다😊


하지만 궁금한 점이 더 생겼다. 비동기 처리에 대해서 배운적이 있는데 병렬처리와 차이가 무엇일까? 동작 방식이 비슷한 것 같지만 명칭이 다른 이유가 있을 것 같았다.

비동기에 대해 더 깊이 있게 알고 싶어 비동기와 동기, block, non-block 등에 대한 차이와 구체적인 내용을 한번 찾아보았다.
이 영상을 보고 이해가 너무 잘되어서 링크를 올린다. 역시 우테코..

간단한 차이를 이야기하자면 비동기는 작업을 위임하고 본인은 다른 작업을 할 수 있게 하는 방식이고 병렬 처리는 다중 스레딩을 통해 병렬적('동시에'라는 말은 쓰지 않겠다. '동시성'과는 또 다르기 때문)으로 여러 작업을 함께 진행하는 방식을 뜻한다.

예를 들어, 사람을 스레드라고 한다면, 내가 집에서 로봇 청소기를 거실에 틀어놓고 나는 주방을 정리하는 것을 비동기로 이야기한다면 나와 내 동생이 분업하여 동생은 거실, 나는 주방을 청소하는 것을 병렬 처리로 이야기할 수 있겠다.

계속 공부할 수록 궁금한것, 알아야할 것이 배로 불어나는 것 같다. 그래도 나름 더 정확하게 알아가는 것 같아 재밌는 것 같다. 그리고 우린 공부하고 새로운 것을 배울 수 있는 최적의 시대에 살고 있다고 생각한다.. 유익한 자료와 영상이 너무 너무 많은 것 같아 행복하다.

P.S. 여기서 캐싱을 사용해서 기존 객체와 결과를 저장해두고 있으면 8초대까지 단축이 가능했다..!

profile
개발은 그저 도구일 뿐

0개의 댓글