포스트

[Asyncio] Python 동시성 관리 (1) - 프로세스와 스레드

Python 에서의 프로세스와 스레드 이론에 대해서 정리


Python 동시성 관리를 이해하기 위해서 알아야 할 것들

  • 프로세스와 스레드 개념
  • Python GIL (Global Interpreter Lock)
  • 코루틴 (Coroutine)

프로세스(Process) 와 스레드(Thread)

프로세스란?

실행중에 있는 프로그램(Program) 을 의미한다. 스케쥴링의 대상이 되는 작업(task)과 같은 의미로 쓰인다. 프로세스 내부에는 최소 하나의 스레드(thread)를 가지고 있는데, 실제로는 스레드(thread) 단위로 스케쥴링을 합니다.

프로그램이란?

프로그램이란, 파일이 저장장치(HDD, SSD) 에 저장되어 있지만, 메모리에는 올라가 있지 않은 상태를 의미한다.

  • 즉, 아직 실행되지 않은 상태이다.

아이콘을 더블클릭(프로그램을 실행)하는 순간 해당 프로그램은 프로세스로 바뀌게 된다. 또한 프로세스는 프로그램의 명령어와 데이터가 메모리에 적재되어 있는 상태 라고도 말할 수 있다.

멀티 프로세싱 이란?

여러 개의 프로세서가 있다면, 여러 프로세스를 동시에 수행 가능하다. 즉, 여러 개의 프로세서가 협력적으로 일을 처리하는 것을 멀티프로세싱(Multi-Processing) 이라고 한다. 즉, 하나의 프로그램을 실행하는데 여러 개의 프로세서가 사용되어 빠르게 처리한다는 것이다

프로세스의 상태

프로세스의 상태의 흐름은 다음과 같이 하나의 그래프 형태로 나타낼 수 있다.

프로세스의 상태는 총 7가지로 나타낼 수 있다.

  • Created
  • Ready
  • Suspended Ready
  • Running
  • Suspended Blocked
  • Asleep(Blocked)
  • Terminated

Created State

프로세스가 생성되는 단계를 의미한다.

  • 가용 메모리 공간 유무에 따라 메모리가 할당된다면 Ready State 로, 할당 받을 수 있는 메모리가 없다면 Suspended Ready State로 대기하게 된다.

Ready State

프로세스(CPU) 할당을 제외한 모든 자원(메모리 등)을 할당 받은 상태를 의미한다. (아직 실행된 것은 아님!)

즉, 프로세서(CPU) 할당을 받게되면 즉시 실행이 가능한 상태이다. CPU 자원 할당을 받으면 Running State 로 가게 되며, 이를 dispatch 라고 한다.

Running Rate

필요한 자원 모두 (CPU, Memory 등…)을 할당 받아 실행 중인 상태를 의미한다.

  • CPU를 차지하고 있는 상태이며, 이 상태에서 실제 작업이 실행되게 된다.

항상 프로세스는 Running 상태에 있는 것이 아니라, 상태가 변화하게 된다. 이 때 Running State 를 빠져나가게 되는 두 가지 경우가 존재한다.

  • Ready State 로 바뀌는 경우
  • Asleep(blocked) State 로 바뀌는 경우

Ready State로 바뀌는 경우는 프로세서의 스케쥴링 우선순위 등으로 인해 CPU 할당을 반납하고 다시 CPU 할당을 대기하는 상태이다. -> Ready 상태로 바뀌는 것을 Preemption 이라고 말한다.

Asleep(blocked) State 로 바뀌는 경우는 프로세서(CPU)외 다른 자원을 기다리는 상태이다. 가장 대표적인 예로 I/O 자원을 할당받기를 기다리고 있는 상태를 말한다.

Asleep(blocked) State

프로세서 외 다른자원 (ex. I/O 자원)을 기다리고 있는 상태를 말한다. 중요한 것은 Asleep 상태에서 다른 자원을 기다리는 것이 완료하면 바로 Running 상태로 가는 것이 아니라, Ready State로 가게된다. 이를 Wake-Up 이라고 한다.

Suspended State

메모리를 할당받지 못한, 혹은 빼앗긴 상태를 의미한다.

크게 Suspended Ready 상태와 Suspended Blocked 상태로 나눌 수 있다.

  • Suspended Ready 는 다른 자원은 할당받았지만 프로세서와 메모리를 할당받지 못한 상태
  • Suspedned Blocked 는 프로세스, 메모리, 기타 자원을 할당받지 못한 상태를 말한다.

메모리를 빼앗기면 일어나는 일 - 빼앗기기 전에 지금까지 수행했던 결과를 memory image 형태로 swap device 에 저장하게 된다.

  • 메모리를 빼앗기는 것을 swap out, 복구하는 것을 swap in 이라고 한다.

Terminted / zombie State

프로세스 수행이 끝난 상태를 의미한다. 프로세스 수행에 필요한 모든 자원 (CPU,메모리 등)을 반납한다.

이때 커널 내에 PCB 라는 정보를 남긴다. 이는 나중에 커널이 PCB 정보를 수집해 기억하기 위함이다.

스레드 (Thread) 란?

어떠한 프로그램 내에서, 특히 프로세스 내에서 실행되는 흐름의 단위를 말한다. 일반적으로 한 프로그램은 하나의 스레드를 가지고 있지만, 프로그램 환경에 따라 둘 이상의 스레드를 동시에 실행할 수 있다. 이러한 실행 방식을 멀티 스레드(Multi-Threads) 라고 한다.

스레드 정의에서 중요한 포인트는, 프로세스 내에서 실행되는 흐름의 단위라고 말하는 것이다.

다른 정의를 좀 더 살펴보면, 스레드(thread) 는 프로세스의 작업을 수행하는 주체이며, 프로세스의 제어 부분을 담당하는 것이라고 한다.

즉, 프로세스는 자원을 할당받아 제어를 수행하면서 목적을 달성하게 되는데, 이때 제어 부분을 담당하는 것이 스레드이다.

프로세스와 스레드의 차이

여기서 중요한 점은 각 프로세스는 별도의 공간에서 실행되고 프로세스끼리는 자원공유를 하지 않는다는 점이다. 이는 다시 말하면 각각의 프로세스는 독립적이다라고 말할 수 있으며, 프로세스 간의 자원 공유를 하려면 별도의 통신이 필요하다.

스레드는 특정 자원을 공유하여 사용하기 때문에 시스템의 자원과 처리 비용이 멀티프로세싱에 비해 어느 정도 감소할 수 있다. 이는 통신의 부담이 적기 때문이다. 하지만 자원을 공유하고 있기 때문에 동시에 스레드(thread)가 같은 자원을 접근시 발생할 수 있는 동기화 문제가 발생한다.

또한, 스레드가 개별로 유기적으로 움직이고 있기 때문에 테스트, 디버깅이 어려울 수 있다. 또, 하나의 스레드의 오류로 전체 프로세스의 문제가 발생할 수 있기 때문에 각별히 신경써야 한다.

결과적으로 멀티 프로세스 대신에 멀티 스레드를 사용하는 이유는 자원 사용의 효율성을 증대시키고, 처리 비용 감소, 응답 시간을 단축시키기 위함이다.

Multi-Thread 로 연산해보기

랜덤으로 생성된 배열을 sorted 내장 함수를 사용하여 정렬하는 연산을 두 번 실행하는 것을 두 가지 방법으로 구현한다.

  1. 단일 스레드로 두 개의 정렬 작업을 연속적으로 실행
  2. 두 개의 스레드가 각각 하나의 작업을 담당하여 실행할 수 있도록 구현
1
2
3
4
5
6
7
8
import time
import threading
import random

list_len = 10_000_000

def working():
	return sorted([random.random() for _ in range(list_len)])

단일 스레드로 두 개의 정렬 작업을 연속적으로 실행

1
2
3
4
%%timeit

sorted_list1 = working()
sorted_list2 = working()
1
8.83 s ± 283 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
  • 약 8.83초가 걸리는 것을 확인할 수 있다.

두 개의 스레드를 사용하는 경우

  • 파이썬에서 스레드를 실행시키기 위해서는 threading 모듈이나 thread 모듈을 사용할 수 있다.
    • threading 모듈은 고수준, thread 모듈은 저수준 모듈이라는 차이를 갖고 있다.
    • 일반적으로 thread 모듈 위에서 구현된 threading 모듈을 사용하고 있다. (thread 모듈은 deprecate) 되어 거의 쓰지 않는다.
  1. threading.Thread() 함수를 호출하여 Thread 객체를 얻는다.
  2. Thread 객체의 start() 메서드를 호출한다.

해당 스레드는 우리가 수행하고자 하는 함수 혹은 메서드를 실행하는데, 일반적으로 구현 방식은 크게

  1. 스레드가 실행할 함수 또는 메소드를 작성하여 사용
  2. threading.Thread 로 부터 파생된 파생클래스를 작성하여 사용 위의 두가지 중 1 을 사용하여 구현한다.
1
2
3
4
5
6
7
threads = []
for i in range(2):
	threads.append(threading.Thread(target=working))
	threads[i].start()

for t in threads:
	t.join() # join 메소드 사용 시, thread 수행될 때 까지 기다림
1
9.66 s ± 213 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
  • threading.Thread 는 리턴값을 받을 수 있는 구조가 기본적으로 제공되지 않는다.
  • 오히려 기존 단일 스레드에서 순차적으로 실행하여 연산한 결과보다 더 오래 걸렸다.
  • 이는 보통 우리가 Python을 설치하여 사용 시 CPython 이라는 파이썬 구현체를 사용하는데, 해당 구현체(인터프리터)는 GIL 이라는 특징을 가지고 있다. (다음 post 에 다루도록 함)
    • 이 때문에 멀티 스레드가 병렬로 연산을 수행하지 못하여 더 오랜시간이 걸린 것이다.

출처 : https://velog.io/@jaebig/python-%EB%8F%99%EC%8B%9C%EC%84%B1-%EA%B4%80%EB%A6%AC-1-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4Process%EC%99%80-%EC%8A%A4%EB%A0%88%EB%93%9CThread

Google AdSense — Post Ad
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

Comments powered by Disqus.