Covenant

파이썬 코드로 보는 멀티스레드



부제: 현직자가 중요하다고 생각하는 기본기



Cover Photo by Luis Gonzalez on Unsplash

인턴으로 일하던 시절 첫 과제로 C#으로 윈도우 애플리케이션을 만들었습니다. 멀티스레드 구현을 잘못하여서 프로그램 내에서 여러 작업을 동시에 하였더니 프로그램이 뻗은 경험이 있습니다. 멀티스레드는 코드몽키가 되는 것이 비전과 사명이 아닌 이상 꼭 알아야할 중요한 개념입니다. 현재 운영체제를 공부하고 있으신 학생분들도 이 부분을 잘 공부하시길 바라며, 그 중요한 멀티스레드를 파이썬 코드와 함께 살펴보도록 하곘습니다.



1. 쓰레드란?



OS를 공부했다면 안본 사람이 없을 명화

꼬꼬마 텔레토비였던 3학년 1학기 학부생 시절을 생각해보면 운영체제에서 인터럽트까지 꾸역꾸역 공부했더니 반겨주는 실지렁이들. 어떻게 실지렁이가 쓰레드가 된다는 말인지, Context Swith로 전환되는 프로세스 안에 또 다른 무엇인가가 있다는 것이 너무나 이상했습니다. 운영체제 그림 중에서 제일 컴퓨터공학 스럽지 않은 그림이라고 생각했습니다.




Youtube음악을 들으면서 웹 서핑을 하고있다면 스레드 공부 끝입니다.


Chrome에서 Youtube에서 음악을 들으면서 웹 서핑을 해본다고 생각해봅니다. 그렇다면 작업 관리창(맥의 경우 활성 상태 보기)에 보이는 Chrome 프로세스안의 생성된 쓰레드가 Youtube를 실행하면서 다른 스레드가 Google 웹 창을 보여주는 것입니다. 즉 스레드를 기준으로 프로세스 내의 실행 흐름 단위 가 생성되게 됩니다.



스레드


  • 프로세스 내에 실행 흐름 단위입니다.
  • 쓰레드는 프로세스에 할당된 메모리, CPU 등의 자원을 사용합니다.
  • Stack만 별도의 메모리를 할당하며 Code, Data, Heap은 쓰레드간 공유합니다.
  • 한 스레드의 결과가 다른 스레드에 영향 끼칩니다. 크롬 하나의 탭에 문제가 생기면 없으면 크롬 자체를 다시 실행해야 하는 경우가 있습니다.
  • 스레드의 경우 디버깅이 어렵기에 동기화 문제는 주의해서 구현해야합니다.


멀티 스레드


  • 한 개의 단일 어플리케이션(응용프로그램)은 여러 스레드로 구성 후 작업 처리해야합니다.
    • 한글에서 싱글 스레드를 사용한다면 프린트를 하는 경우 문서 수정은 불가능할 것입니다.
  • 프로세스를 생성하는 것은 고비용입니다. 스레드를 사용한다면 시스템 자원 소모 감소 및 처리량 증가시킬 수 있습니다.
  • 쓰레드는 이미 공유하고 있기에 프로세스를 사용했다면 생길 통신 부담이 감소합니다.
  • 멀티 스레드를 사용할 경우 디버깅이 어렵습니다. 자원 공유 문제(일명 교착상태)가 생깁니다.


1-1. 파이썬 코드로 스레드 구현.

import logging
import threading
import time

# 스레드에서 실행할 함수
def work(name):
    logging.info("[Sub-Thread] %s: 시작합니다.", name)
    time.sleep(3) # 3초간 sleep합니다.
    logging.info("[Sub-Thread] %s: 종료합니다.", name)

# 메인 영역
if __name__ == "__main__": # main thread 흐름을 타는 시작점
    format = "%(asctime)s: %(message)s" # Logging format 설정
    logging.basicConfig(format=format, level=logging.INFO, datefmt="%H:%M:%S")

    x = threading.Thread(target=work, args=('A',)) 

    logging.info("[Main-Thread] 쓰레드 시작 전")

    x.start() # 서브 스레드 시작

    logging.info("[Main-Thread] 프로그램을 종료합니다.")
  • logging.basicConfig는 log 형식을 지정하기 위해서 정의한 코드입니다.
  • threading.Thread 란?
    • Target: 스레드가 실행할 함수(일명 callable object)를 지정해줍니다. 여기서는 work 함수를 실행합니다.
    • args: 함수에 넘길 인자입니다. work함수의 name에 인자 값으로 "A"를 줍니다.
  • .start() 호출할 경우 스레드 활동을 시작합니다.


1-2. 실행 결과

18:56:12: [Main-Thread] 쓰레드 시작 전
18:56:12: [Sub-Thread] A: 시작합니다.
18:56:12: [Main-Thread] 프로그램을 종료합니다.
18:56:15: [Sub-Thread] A: 종료합니다.
  • work()함수에서 3초 쉬기에 메인 스레드가 종료되고 Sub-Thread가 종료됩니다.


1-3. 실행 결과 도식화



  • [1] a.start()가 실행되면 스레드 활동이 시작됩니다.
  • [2] Sub Thread A는 sleep(3)이 실행되면 3초 실행을 멈춥니다.
  • [3] Sub Thread가 멈춘 사이 Main Thread는 종료됩니다.
  • [4] Sub Thread가 종료됩니다.


2. 쓰레드 + join()


Main이 종로되지 않고 실행하는 스레드가 종료 될 때까지 기다리게 하는 방법이 있습니다. 바로 join()을 사용하는 것입니다. join() 메서드가 호출된 스레드가 종료될 때까지 호출하는 스레드를 블록 합니다. 즉 Main Thread에서 Sub Thread를 join한다면 Sub Thread가 종료될때까지 기다립니다.


2-1. 파이썬 코드로 구현

import logging
import threading
import time

# 스레드에서 실행할 함수
def thread_func(name):
    logging.info("[Sub-Thread] %s: 시작합니다.", name)
    time.sleep(3) # 3초간 sleep합니다.     
    logging.info("[Sub-Thread] %s: 종료합니다.", name)

# 메인 영역
if __name__ == "__main__": # main thread 흐름을 타는 시작점
    format = "%(asctime)s: %(message)s" # Logging format 설정
    logging.basicConfig(format=format, level=logging.INFO, datefmt="%H:%M:%S")
    logging.info("[Main-Thread] 쓰레드 시작 전")

    x = threading.Thread(target=thread_func, args=('A',)) 

    logging.info("[Main-Thread] 쓰레드 시작 전")

    a.start() # 서브 스레드 시작

    logging.info("[Main-Thread] 쓰레드 종료를 기다립니다.")

    a.join() # join() 추가!!!

    logging.info("[Main-Thread] 프로그램을 종료합니다.")


2-2. 실행 결과

19:14:31: [Main-Thread] 쓰레드 시작 전
19:14:31: [Main-Thread] 쓰레드 시작 전
19:14:31: [Sub-Thread] A: 시작합니다.
19:14:31: [Main-Thread] 쓰레드 종료를 기다립니다.
19:14:34: [Sub-Thread] A: 종료합니다.
19:14:34: [Main-Thread] 프로그램을 종료합니다.
  • Sub Thread가 종료된 다음 Main Thread가 종료되는 것을 볼 수 있습니다.


2-3. 실행 결과 도식화




  • [1] Main Thread에서 a.join()을 실행하면 Sub Thread가 끝날때까지 기다립니다.
  • [2] Sub Thread가 끝나면 Main Thread가 실행됩니다.


3. 데몬스레드


3-1. 데몬쓰레드란?


데몬스레드는 카카오 첫 공채 블라인드 2차 필기 시험에 나온 주제입니다. 지금까지 Sub Thread는 Main Thread가 종료되더라도 끝까지 실행되거나 심지어 Main Thread가 Sub Thread가 종료되기를 기다리게 할 수 있었습니다. 그러나 데몬쓰레드는 Main Thread가 종료되면 즉시 종료되는 스레드입니다.


  • 백그라운드에서 실행하는 스레드입니다.
  • 메인스레드 종료시 즉시 종료됩니다.
  • 워드에서 자동저장 기능을 데몬 스레드로 만들 수 있습니다. 워드를 종료하면 자동저장 데몬 스레드는 바로 소멸됩니다.
  • 데몬 스레드가 아닌 일반 스레드는 작업 종료시 까지 실행됩니다.


3-2. 파이썬 코드로 구현

import logging
import threading
import time

# 스레드 실행 함수
def work(name, d):
    logging.info("[Sub-Thread] %s: 시작합니다.", name)

    for i in d:
        print(i)

    logging.info("[Sub-Thread %s: 종료합니다.", name)

# 메인 영역
if __name__ == "__main__":
    # Logging format 설정
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO, datefmt="%H:%M:%S")

    # daemon을 True로 줍니다.
    x = threading.Thread(target=work, args=('A', range(100)), daemon=True) 
    y = threading.Thread(target=work, args=('B', range(100)), daemon=True)

    logging.info("[Main-Thread] 쓰레드 실행 전")

    # 서브 스레드 시작
    x.start()
    y.start()

    logging.info("[Main-Thread] 프로그램을 종료합니다.")
  • threading.Thread에 daemon=True를 주면 데몬스레드로 생성됩니다.
  • x.isDaemon()이 True라면 데몬스레드입니다.
  • 데몬스로드로 생성을 하지 않았더라도 x.daemon = True 한다면 데몬스레드로 만들 수 있습니다.


3-3. 실행 결과

19:24:56: [Main-Thread] 쓰레드 실행 전
19:24:56: [Sub-Thread] A: 시작합니다.
A:0
19:24:56: [Sub-Thread] B: 시작합니다.
B:0
A:1
B:1
19:24:56: [Main-Thread] 프로그램을 종료합니다.


3-4. 실행 결과 도식화



  • work함수의 인자로 range(100)이 들어갔기에 데몬스레드가 아니라면 1부터 100까지 출력되야 합니다.
  • 그러나 Main Thread가 종료된다면 데몬 스레드가 종료되기에 A 데몬 스레드는 1까지만 출력, B는 0까지만 출력하고 종료되었습니다.
  • 결과는 코드를 실행하는 운영체제, CPU 환경에 따라서 조금씩 다르게 나올 수 있습니다.



Ref.