운영체제

멀티 스레드 & 멀티 프로세스 실험하기 with Python

jamie-lee 2023. 4. 16. 20:57

멀티 스레드과 멀티 프로세스 개념을 공부하다가 파이썬으로 직접 테스트해보면 좋을 것 같아 공부해보았다.

멀티 스레드과 멀티 프로세스의 차이 요약

  1. 동시에 엑셀 파일을 여러 개 연다 = 멀티 프로세스
  2. 한 엑셀 파일에서 데이터 정렬, 필터링 작업, 함수 계산 등을 동시에 처리한다 = 멀티 스레드
  3. 멀티 프로세스 환경에서는 각 프로세스는 다른 프로세스의 작업에 영향을 받지 않음 (내가 a라는 엑셀 파일에 작업하는데 b파일의 내용이 수정되거나 하지 않으니…)
  4. 멀티 스레드 환경에서는 유저가 동시에 여러 작업을 수행할 수 있으나, 이 작업 간 동기화가 주요 이슈가 된다.
  5. 멀티 프로세싱 환경에서는 한 프로세스가 깨져도 다른 프로세스에 영향 없음

멀티 스레드 파이썬으로 실험하기

import threading
import time


def print_numbers():
    for i in range(10):
        time.sleep(1)  # 1초 마다 숫자를 출력한다
		print(i)


def print_letters():
    for letter in '가나다라마바사아자차':
        time.sleep(1)  # 1초 마다 문자를 출력한다
		print(letter)


lock = threading.Lock()

# 두 개의 스레드를 만든다
t1 = threading.Thread(target=print_numbers)  # print_numbers를 호출하는 스레드
t2 = threading.Thread(target=print_letters)  # print_letters를 호출하는 스레드

# 스레드를 시작시킨다
t1.start()
t2.start()

# 두 스레드가 끝날 때까지 기다린다
t1.join()
t2.join()

print("테스트 종료!")

출력 결과

Pasted image 20230416205317.png
두 스레드가 이렇게 동시적으로 출력 작업을 실시한다.
호출할 때마다 결과가 동일하지 않다 → 결과 예측이 안 된다

멀티 프로세스 파이썬으로 실험하기

import multiprocessing
import time

lock = multiprocessing.Lock()


def print_numbers():
    for i in range(10):
        time.sleep(1)  # 1초 마다 숫자를 출력한다
		print(i)


def print_letters():
    for letter in '가나다라마바사아자차':
        time.sleep(1)  # 1초 마다 문자를 출력한다
		print(letter)


# 프로세스를 생성하는 작업은 메인 코드 블럭에 넣어야 한다
if __name__ == '__main__':

    # 두 개의 프로세스를 만든다
    # print_numbers를 호출하는 프로세스
    p1 = multiprocessing.Process(target=print_numbers)
    # print_letters를 호출하는 프로세스
    p2 = multiprocessing.Process(target=print_letters)

    # 프로세스를 시작시킨다
    p1.start()
    p2.start()

    # 두 프로세스가 끝날 때까지 기다린다 
    p1.join()
    p2.join()

    print("테스트 종료!")

  • 프로세스 개수가 많아지면 리스트에 추가하고, for문을 이용해 join() 메소드를 호출시킬 수 있다.

유의할 점

출력에 관해 얘기하기 전에 멀티 스레드 예제와 거의 비슷한데 살짝 다른 점이 있다! 바로 아래 부분이다.

# 프로세스를 생성하는 작업은 메인 코드 블럭에 넣어야 한다
if __name__ == '__main__':

이 지시를 따르지 않으면 아래와 같은 RuntimeError가 뜨면서 코드가 제대로 실행되지 않는다.

raise RuntimeError('''
RuntimeError:
        An attempt has been made to start a new process before the        
        current process has finished its bootstrapping phase.

        This probably means that you are not using fork to start your     
        child processes and you have forgotten to use the proper idiom    
        in the main module:

            if __name__ == '__main__':
                freeze_support()     
                ...

        The "freeze_support()" line can be omitted if the program
        is not going to be frozen to produce an executable.

대략 요약하자면 자식 프로세스를 만들기 위해 fork를 사용하지 않고 있으므로, 메인 코드 블럭(if __name__ == '__main__':) 을 사용하라는 이야기다. 이는 예상치 못한 에러라 의아했는데 다음과 같은 숨겨진(?) 사연이 있었다.

윈도우 운영체제는 리눅스 같은 유닉스 계열 운영체제와 달리 새로운 프로세스를 만들 때 fork를 사용하지 않는다. 윈도우의 프로세스 생성 메커니즘은 spawn이라고 한다.

spawn을 사용하면 새로운 프로세스가 생성되고 해당 프로세스에서 파이썬의 메인 모듈(그러니까 내가 실행한 저 코드, 파이썬 인터프리터로 직접 실행된 스크립트 파일)를 다시 로드한다. 이 과정에서 메인 모듈 안의 모든 코드가 새 프로세스에서 다시 실행되면서, 예상치 못한 에러가 발생하는 것이다.

[!INFO] 메인 모듈이란? 메인 모듈이 아닌 경우는?
예를 들어, 쉘에서 python my_script.py와 같은 파일을 실행했다고 치면, 이때 my_script.py 파일이 메인 모듈이다.

메인 모듈이 아닌 경우는 다른 모듈에서 import를 통해 불러오는 경우가 있다.
예를 들어, my_script.py 파일에서 import another_module 이라고 적으면, 이때 이 another_module.py는 메인 모듈이 아닌 다른 모듈로서 로드되는 것이다.

if __name__ == '__main__': 구문을 사용하면 메인 모듈로 실행될 때만 블럭 안의 코드를 실행할 수 있게 한다.

이러한 부작용을 방지하기 위해 if __name__ == '__main__': 블록을 사용한다.

이 조건문의 의미는 __name__ 변수가 __main__일때만 다음 코드를 실행하라는 뜻이다. (파이썬의 스크립트가 직접 실행되면 __name__ 변수에 "__main__"이 할당되고, 만약 모듈이 임포트되면 해당 모듈의 이름이 할당된다.)

따라서 이 블록 안의 코드는 스크립트가 메인 모듈로 실행될 때만 실행된다. 새 프로세스에서 메인 모듈이 다시 로드되거나, 혹은 이 스크립트가 다른 파일로 임포트되어 실행될 때와 같은 경우에는 해당 코드가 실행되지 않는다. 원치 않는 코드 실행을 방지할 수 있고 부작용을 막을 수 있다.

출력 결과

Pasted image 20230416204714.png
스레드의 경우보다 규칙적으로 p1 -> p2 순으로 번갈아가며 출력 작업을 실시한다.
하지만 중요한 것은 완벽하진 않다! (프로세스의 실행 순서 또한 운영체제의 스케줄러에 의존하고 있기 때문에) 그치만 대부분의 경우 저렇게 출력되며, 멀티 스레드보다는 비교적 예측 가능한 결과를 보였다. 신기방기.

'운영체제' 카테고리의 다른 글

운영체제 개념 노트  (0) 2022.12.16