IBM®
메인 컨텐츠로 가기
    Korea [국가변경]    이용약관
 
 
   
        제품    서비스 & 솔루션    고객지원 & 다운로드    회원 서비스    
메인 컨텐츠로 가기

한국 developerWorks  >  AIX and UNIX  >

파이썬을 사용한 실전 스레드 프로그래밍

threading 사용법 패턴

developerWorks
문서 옵션

JavaScript가 필요한 문서 옵션은 디스플레이되지 않습니다.

샘플 코드

영어원문

영어원문


제안 및 의견
피드백

난이도 : 중급

Noah Gift, Software Engineer, Giftcs

옮긴이: 박재호 이해영 dwkorea@kr.ibm.com

2008 년 10 월 14 일

파이썬에서 스레드 프로그래밍은 큐(Queues)와 스레드를 혼합하는 방법으로 복잡성을 최소로 줄여서 수행할 수 있습니다. 이 기사는 스레드와 큐를 함께 사용해서 동시성을 요구하는 문제를 해결하는, 간단하면서도 효과적인 패턴을 만들어내는 방법을 탐구합니다.

소개

파이썬을 사용하면, 동시성 옵션에 부족함이 없다. 표준 라이브러리는 스레드, 프로세스, 비동기식 I/O를 포함한다. 많은 경우에 있어 파이썬은 asynchronous, threading, subprocess와 같은 고수준 모듈을 생성하는 방법으로 다양한 동시성 메서드를 활용하는 어려움을 상당수 제거해왔다. 표준 라이브러리 외부에서 두세 가지 예를 들자면 twisted, stackless, processing 모듈과 같은 외부 해법이 존재한다. 이 기사에서는 실제 예를 들어 파이썬에서 스레드 프로그래밍 방법을 전문적으로 다룬다. 스레드 API를 문서로 만든 훌륭한 온라인 자료가 있지만, 이 기사는 공통적인 threading 사용법 패턴이라는 실질적인 예를 제공하려고 시도한다.

GIL(Global Interpretor Lock)은 파이선 해석기가 스레드 안전이 아니라는 사실을 의미한다. 파이썬 객체에 안전하게 접근하려면 현재 스레드가 전역 락을 쥐고 있어야 한다. 단지 스레드 하나만 파이썬 Objects/C API를 획득할 수 있으므로, 해석기는 주기적으로 매 100바이트 코드 명령마다 잠금을 풀었다 다시 얻는다. sys.setcheckinterval() 함수는 해석기가 스레드 전환을 점검하는 주기를 제어한다.

또한 잠금을 해제하고 다시 얻는 과정에서 잠재적인 차단 I/O 연산이 개입한다. 참고자료에 정리한 GIL 관련 자료를 참조하기 바란다.

GIL 때문에 CPU에 밀접한 응용은 스레드로 도움이 되지 않는다는 중요한 사실을 짚고 넘어가겠다. 파이썬에서는 프로세스를 사용하거나 프로세스와 스레드를 섞어서 사용하는 방법을 권장한다.

우선 프로세스와 스레드의 차이점을 정의하고 넘어가는 편이 좋겠다. 스레드는 상태, 메모리, 자원을 공유한다는 측면에서 프로세스와 다르다. 이렇게 단순한 차이는 스레드에 있어 강점이자 약점이다. 스레드는 경량이며 통신이 쉬운 반면에 데드락, 경쟁 조건, 엄청난 복잡성을 포함한 수많은 문제 양산소이기도 하다. 다행스럽게 GIL과 큐잉 모듈을 사용하면, 파이썬에서 스레드는 다른 언어보다 구현이 훨씬 더 간단해진다.

파이썬 스레드 안녕!

이 기사에서는 파이썬 2.5 이상 버전을 설치했다고 가정한다. 여기서 다루는 많은 예는 최소한 파이썬 2.5부터 등장한 파이썬 언어가 제공하는 새로운 기능을 사용할 계획이기 때문이다. 파이썬으로 스레드를 시작하기 위해 간단한 "Hello World" 예제로 시작한다.


hello_threads_example
                
        
        import threading
        import datetime
        
        class ThreadClass(threading.Thread):
          def run(self):
            now = datetime.datetime.now()
            print "%s says Hello World at time: %s" % 
            (self.getName(), now)
        
        for i in range(2):
          t = ThreadClass()
          t.start()
      

이 예제를 돌리면, 다음과 같은 결과물이 나온다.

      # python hello_threads.py 
      Thread-1 says Hello World at time: 2008-05-13 13:22:50.252069
      Thread-2 says Hello World at time: 2008-05-13 13:22:50.252576
      

이 결과를 살펴보면, 날짜 스탬프가 찍힌 두 스레드에서 나온 Hello World 문을 확인할 수 있다. 실제 코드를 들여다보면, 두 가지 import 구문이 있다. 하나는 datetime 모듈 임포트이며, 다른 하나는 threading 모듈 임포트다. 클래스 ThreadClassthreading.Thread에서 상속되며, 이런 이유 때문에 스레드 내부에서 동작할 코드를 수행하는 run 메서드를 정의할 필요가 있다. run 메서드에서 유일하게 주목할 중요한 사항은 스레드 이름을 확인하는 메서드인 self.getName()이다.

마지막 코드 세 줄은 실제 클래스를 호출하고 스레드를 시작한다. 눈치챘겠지만 t.start()는 스레드를 실제로 시작하는 부분이다. threading 모듈은 상속을 염두에 두고 설계되었으며, 실제로 저수준 thread 모듈 위에서 만들어졌다. 대다수 경우에, threading.Thread에서 상속받는 방법을 우수 개발 사례로 간주한다. 스레드 프로그래밍을 위한 아주 자연스러운 API를 생성하기 때문이다.

스레드와 함께 큐 활용

직전에 언급했듯이 threading은 스레드가 자료나 자원을 공유할 필요가 있을 때 복잡해질 수 있다. threading 모듈은 세마포어, 조건 변수, 이벤트, 잠금 등과 같은 여러 가지 동기화 요소를 제공한다. 이런 옵션이 존재하지만, 이런 옵션을 대신해서 큐를 사용하는 방법을 우수 개발 사례로 간주한다. 큐는 훨씬 더 다루기 쉽고, 스레드 프로그램을 상대적으로 안전하게 만든다. 단일 스레드로 자원에 대한 모든 접근을 효과적으로 허용하며, 좀 더 깔끔하고 읽기 쉬운 디자인 패턴을 허용하기 때문이다.

다음 예제에서, 직렬로 하나씩 차례로 웹 사이트 URL을 가져와 페이지 중 첫 1024바이트를 출력하는 프로그램을 만들어보겠다. 이는 스레드를 사용해 빨리 뭔가를 수행하는 고전적인 예다. 먼저 urllib2 모듈을 사용해 한번에 한 페이지씩 가져와 경과 시각을 출력하자.


직렬로 URL 가져오기
                
        import urllib2
        import time
        
        hosts = ["http://yahoo.com", "http://google.com", "http://amazon.com",
        "http://ibm.com", "http://apple.com"]
        
        start = time.time()
        # 호스트 URL을 가져와 페이지 중 첫 1024바이트를 출력한다.
        for host in hosts:
          url = urllib2.urlopen(host)
          print url.read(1024)
        
        print "Elapsed Time: %s" % (time.time() - start)
      

이런 작업을 할 때, 표준 출력으로 수 많은 출력을 내보낸다. 페이지 일부를 출력해야 하기 때문이다. 하지만 마지막에 다음과 같은 결과를 얻을 것이다.

        Elapsed Time: 2.40353488922  
        

코드를 잠깐 살펴보자. 단지 모듈 둘만 임포트했다. 우선 묵직한 기능을 제공하는 urllib2 모듈은 웹 페이지를 가져오는 작업을 수행한다. 둘째로 time.time()을 호출해 시작 시간값을 생성하고 다시 한번 이 메서드를 불러 얻은 값에서 초기 값을 빼는 방법으로 프로그램 실행에 걸린 시간을 계산한다. 마지막으로 프로그램 속력을 살펴보면, "2.5초"라는 결과는 그리 나쁘지 않다. 하지만 인출할 웹 페이지가 수백여 개에 이를 경우 현재 평균값을 고려해 보자면 거의 50초 정도 걸린다. 스레드 버전으로 속력을 높이는 방법을 살펴보자.


스레드로 URL 가져오기
                
          #!/usr/bin/env python
          import Queue
          import threading
          import urllib2
          import time
          
          hosts = ["http://yahoo.com", "http://google.com", "http://amazon.com",
          "http://ibm.com", "http://apple.com"]
          
          queue = Queue.Queue()
          
          class ThreadUrl(threading.Thread):
          """Threaded Url Grab"""
            def __init__(self, queue):
              threading.Thread.__init__(self)
              self.queue = queue
          
            def run(self):
              while True:
                # 큐에서 호스트를 가져온다.
                host = self.queue.get()
            
                # 호스트 URL을 가져와 페이지 중 첫 1024바이트를 출력한다.
                url = urllib2.urlopen(host)
                print url.read(1024)
            
                # 작업 완료를 알리기 위해 큐에 시그널을 보낸다.
                self.queue.task_done()
          
          start = time.time()
          def main():
          
            # 스레드 풀을 만들어 큐 인스턴스를 전달한다.
            for i in range(5):
              t = ThreadUrl(queue)
              t.setDaemon(True)
              t.start()
              
           # 큐에 자료를 밀어넣는다.
              for host in hosts:
                queue.put(host)
           
           # 모든 사항을 처리할 때까지 큐 완료를 기다린다.
           queue.join()
          
          main()
          print "Elapsed Time: %s" % (time.time() - start)
      

이 예제는 설명할 코드가 조금 더 많다. 하지만 첫 번째 스레드 예제와 비교해서 그리 복잡하지는 않다. queuing 모듈을 사용했기 때문이다. 이 패턴은 아주 일반적이며, 파이썬에서 스레드를 사용할 때 권장하는 방식이다. 단계는 다음과 같다.

  1. Queue.Queue() 인스턴스를 만들어 자료를 밀어넣는다.
  2. threading.Thread에서 상속받아 생성한 threading 클래스에 밀어넣은 자료의 인스턴스를 넘긴다.
  3. 데몬 스레드 풀을 만든다.
  4. 한번에 큐에서 항목 하나씩 끌어당겨서 작업을 위해 스레드 내부에 있는 자료와 run 메서드를 사용한다.
  5. 작업을 마치면, 작업이 끝났음을 알려주는 queue.task_done()으로 큐에 시그널을 보낸다.
  6. 큐에 join()해서 큐가 빌 때까지 기다린 다음에 메인 프로그램을 종료한다.

이 패턴에 대해 주목하자. 데몬 스레드를 참으로 설정하는 방법으로 메인 스레드나 프로그램이 데몬 스레드가 살아 있는 경우에만 종료하게 만든다. 이는 프로그램 흐름을 제어하는 간단한 방식이다. 종료 전에 큐가 빌 때까지 join()으로 기다릴 수 있기 때문이다. 정확한 처리 과정은 queue 모듈 문서에 가장 잘 나타나있다. 참고자료를 살펴보기 바란다.

join()
"큐에 있는 모든 항목을 얻어서 처리할 때까지 차단한다. 끝나지 않은 태스크 카운터는 큐에 아이템을 추가할 때마다 증가한다. 카운터는 항목을 인출해서 해당 작업이 완료되었다는 사실을 알려주는 task_done()을 소비자 스레드가 호출할 때마다 감소한다. 완료되지 않은 태스크가 0으로 떨어지면, join()은 차단을 해제한다."



위로


다중 큐로 작업하기

위에서 보여준 패턴은 아주 효과적이므로, 큐에 스레드 풀을 추가하도록 변경하는 방식으로 이를 확장하기도 상대적으로 쉽다. 위에서 제시한 예제에서, 웹 페이지의 첫 부분을 출력하도록 했다. 다음 예제는 이렇게 하는 대신 각 스레드가 가져온 전체 웹 페이지를 반환해 다른 큐에 밀어 넣는다. 그러고 나서 다른 스레드 풀을 설정해 둘째 큐에 join()하도록 만들고 웹 페이지에 대한 작업을 진행한다. 이 예제에서 수행한 작업은 Beautiful Soup이라는 외부 파이썬 모듈을 사용해 웹 페이지 내용을 해석한다. 이 모듈을 사용할 경우 단지 몇 줄이면, 방문한 각 페이지에 대한 제목(title) 태그를 추출해 출력할 수 있을 것이다.


다중 큐로 웹 사이트 데이터 마이닝하기
                
import Queue
import threading
import urllib2
import time
from BeautifulSoup import BeautifulSoup

hosts = ["http://yahoo.com", "http://google.com", "http://amazon.com",
        "http://ibm.com", "http://apple.com"]

queue = Queue.Queue()
out_queue = Queue.Queue()

class ThreadUrl(threading.Thread):
    """Threaded Url Grab"""
    def __init__(self, queue, out_queue):
        threading.Thread.__init__(self)
        self.queue = queue
        self.out_queue = out_queue

    def run(self):
        while True:
            # 큐에서 호스트를 가져온다.
            host = self.queue.get()

            # 호스트 URL을 가져와 웹 페이지를 가져온다.
            url = urllib2.urlopen(host)
            chunk = url.read()

            # 큐에 웹 페이지를 밀어넣는다.
            self.out_queue.put(chunk)

            # 작업 완료를 알리기 위해 큐에 시그널을 보낸다.
            self.queue.task_done()

class DatamineThread(threading.Thread):
    """Threaded Url Grab"""
    def __init__(self, out_queue):
        threading.Thread.__init__(self)
        self.out_queue = out_queue

    def run(self):
        while True:
            # 큐에서 페이지를 가져온다.
            chunk = self.out_queue.get()

            # 페이지를 해석한다.
            soup = BeautifulSoup(chunk)
            print soup.findAll(['title'])

            # 작업 완료를 알리기 위해 큐에 시그널을 보낸다.
            self.out_queue.task_done()

start = time.time()
def main():

    # 스레드 풀을 생성하고 큐 인스턴스를 넘긴다.
    for i in range(5):
        t = ThreadUrl(queue, out_queue)
        t.setDaemon(True)
        t.start()

    # 큐에 자료를 밀어넣는다.
    for host in hosts:
        queue.put(host)

    for i in range(5):
        dt = DatamineThread(out_queue)
        dt.setDaemon(True)
        dt.start()


    # 모든 사항을 처리할 때까지 큐 완료를 기다린다.
    queue.join()
    out_queue.join()

main()
print "Elapsed Time: %s" % (time.time() - start)


이 스크립트 버전을 실행하면, 결과는 다음과 같다.

  # python url_fetch_threaded_part2.py 

  [<title>Google</title>]
  [<title>Yahoo!</title>]
  [<title>Apple</title>]
  [<title>IBM United States</title>]
  [<title>Amazon.com: Online Shopping for Electronics, Apparel,
 Computers, Books, DVDs & more</title>]
  Elapsed Time: 3.75387597084

코드를 보면 또 다른 큐 인스턴스를 추가했으며, 첫 스레드 풀 클래스인 ThreadURL로 큐를 전달했음을 알 수 있다. 그러고 나서 다음 스레드 풀 클래스인 DatamineThread를 위해 정확히 똑같은 구조체를 복사했다. 이 클래스의 run 메서드에서 웹 페이지를 가져온 다음에 각 스레드가 큐에서 저장된 페이지를 가져와 Beautiful Soup으로 이 페이지를 처리한다. 이 경우 Beautiful Soup을 사용해 각 페이지에 있는 제목 태그(title)를 인출한 다음에 출력한다. 이 예제는 뭔가 좀 더 유용한 기능을 추가하기가 상당히 쉬우므로, 검색 엔진이나 데이터 마이닝 도구를 위한 기본 틀로 사용할 수 있다. Beautiful Soup을 사용해 각 페이지로부터 링크를 추출해 따라가는 기능도 생각할 수 있다.




위로


요약

이 기사는 파이썬에서 스레드를 살펴보고 복잡성과 미묘한 오류를 해결하고 읽기 쉬운 코드를 생성하기 위해 큐를 사용하는 우수 개발법을 제시했다. 이런 기본적인 패턴은 상대적으로 간단한 반면, 큐와 스레드 풀을 함께 변경하는 방법으로 광범위한 문제를 해결하는 데 사용할 수 있다. 첫 절에서 향후 프로젝트를 위한 모델로서 좀 더 복잡한 처리 파이프라인을 생성하는 방법을 설명하는 글로 시작했다. 참고자료 절에 일반적인 동시성과 스레드에 대한 훌륭한 자료를 정리해 놓았다.

마지막으로 스레드는 모든 문제를 푸는 해법이 될 수 없으며 프로세스가 많은 경우에 상당히 적합한 해법이라는 사실을 반드시 짚고 넘어가야 한다. 특히 표준 라이브러리인 subprocess 모듈은 여러 프로세스를 fork해 반응을 듣기만 해도 되는 경우에 훨씬 더 쓰기 쉽다. 여기에 대한 공식 문서는 참고자료 절을 살펴보기 바란다.





위로


다운로드 하십시오

설명이름크기다운로드 방식
이 글의 예제 스레딩 코드threading_code.zip24KBHTTP
다운로드 방식에 대한 정보


참고자료

교육

제품 및 기술 얻기

토론


필자소개

Photo of Noah GIft

Noah Gift는 오라일리에서 나온 "Python For Unix and Linux" 공동 저자다. Gift는 저자이자 연사이자 컨설턴트이자 공동체 리더로 IBM developerWorks, Red Hat Magazine, 오라일리, MacTech에 기고한다. Gift가 운영하는 컨설팅 회사 웹 사이트는 www.giftcs.com이며, 개인 웹 사이트는 www.noahgift.com이다. Noah는 또한 아틀란타 파이썬 사용자 그룹 홈 페이지인 www.pyatl.org를 담당하는 현재 조직장이다. Gift는 칼 스테이트 로스 엔젤레스에서 CIS 석사 학위를 받았으며, 칼 폴리 산 루이스 오비스포에서 영양학 학사 학위를 받았다. 애플과 LPI 인증 시스템 관리자이며 칼텍, 디즈니 피처 애니메이션, 소니 이미지웍스, 터너 스튜디오에서 일한다. 남는 시간에는 부인 Leah, 아들 Liam과 함께 피아노를 연주하며 종교적인 수양을 하면서 보낸다.




기사에 대한 평가


보다 나은 서비스를 제공하기 위함이오니 잠시 짬을 내어 이 양식을 제출하여 주십시오.



 


 


 


이 문서 북마킹 하기

mar.gar.in mar.gar.in naver naver eolin eolin del.icio.us del.icio.us





위로


developerWorks 콘텐트를 다른 사이트에 전재하기:
developerWorks 콘텐트에 대한 저작권은 IBM에 있습니다. IBM의 서면 허가나 원본 저자의 허락이 없이는 전재를 금합니다. 저희 콘텐트를 전재하시려면 IBM developerWorks 담당자 에게 문의하십시오.
    IBM 소개 개인정보 보호정책 문의