프로세스와 스레드
프로세스
프로세스란 컴퓨터에서 연속적으로 실행되고 있는 컴퓨터 프로그램을 말합니다. 종종 스케줄링의 대상이 되는 작업이라는 용어와 거의 같은 의미로 쓰입니다. 여러 개의 프로세서를 사용하는 것을 멀티프로세싱이라고 하며 같은 시간에 여러 개의 프로그램을 띄우는 방식을 멀티 태스킹이라고 합니다.
스레드
스레드란 어떠한 프로그램 내에서, 특히 프로세스 내에서 실행되는 흐름의 단위를 말합니다. 일반적으로 한 프로그램은 하나의 스레드를 가지고 있지만, 프로그램 환경에 따라 둘 이상의 스레드를 동시에 실행할 수 있습니다. 이런 방식을 멀티 스레드라고 합니다.
멀티 프로세스와 멀티 스레드
멀티 프로세스
멀티 프로세스는 별도의 메모리 영역을 가지며, 특별한 메커니즘으로만 통신할 수 있습니다. 프로세서는 각 스레드에 대해 별도의 레지스터 집합을 불러오거나 저장하는데, 프로세스 간 데이터 공유와 통신용으로는 비효율적이다. 파이썬에서는 멀티 프로세스 방식에 subprocess 모듈을 사용한다.
멀티 스레드
단일 프로세스 내의 멀티 스레드는 동일한 메모리에 접근한다. 스레드는 데이터 공유를 통해 간단하게 통신하는데, threading 모듈의 처리를 통해 한번에 한 스레드만 메모리 영역에 접근할 수 있습니다. 각 프로세스가 독립적인 스택, 힙, 코드, 데이터 영역을 가지는 반면, 한 프로세스에 속한 스레드는 스택 영역을 제외한 메모리 영역을 공유합니다.
⭐️ 파이썬에서 스레드는 있긴 하지만, 진정한 병렬 실행이 지원되는 것은 아닙니다. 하지만 프로세스는 병렬로 사용하는 것이 가능한데 이것을 통해 코드를 한번 구현해 보겠습니다.
동시성과 병렬성
동시성
논리적으로 여러 작업이 동시에 실행되는 것처럼 보이는 것입니다. 예를 들어 I/O(파일 및 네트워크 소켓 입력 및 출력) 연산 등은 프로그램의 흐름에 큰 짐이 될 수 있습니다. 이럴 때 한 작업의 I/O 연산이 완료되기를 기다리는 동안 다른 작업을 수행하여 유휴 시간을 활용 한다는 뜻을 가지고 있습니다.
병렬성
병렬성이란 물리적으로 여러 작업이 동시에 처리되는 것입니다. 데이터 병렬성과 작업 병렬성으로 나눌 수 있습니다. 데이터 병렬성은 같은 작업을 병렬처리하는 것입니다. 하나의 커다란 작업에서 전체 데이터를 쪼갠 후 병렬 처리하면 작업을 빠르게 수행할 수 있습니다. 작업 병렬성은 서로 다른 작업을 처리하는 것입니다. 웹 서버에서는 다수의 독립적인 요청을 병렬로 개별적으로 처리할 수 있습니다.
결론
- 동시성은 싱글코어 환경에서 여러 작업이 동시에 실행되는 것 처럼 보인다.(논리적)
- 병렬성은 멀티코어를 통해 여러 작업을 동시에 처리 합니다. (물리적)
코드 구현하기
Subprocess 모듈
subprocess 모듈은 '부모-자식' 프로세스 쌍을 생성하는 데 사용됩니다. 부모 프로세스는 사용자에 의해 실행됩니다. 부모 프로세스는 차례로 다른 일을 처리하는 자식 프로세스의 인스턴스를 실행합니다. 자식 프로세스를 사용함으로써, 멀티 코어의 이즘얼 최대한 취하고, 동시성 문제를 운영 체제가 알아서 처리하도록합니다. 먼저 간단한 예제 코드는 아래와 같습니다.
>>> import subprocess
>>> subprocess.run(["echo", "이것은 subprocess입니다."])
이것은 subprocess입니다.
CompletedProcess(args=['echo', '이것은 subprocess입니다.'], returncode=0)
>>> subprocess.run(["sleep", '5'])
CompletedProcess(args=['sleep', '5'], returncode=0)
이런식으로 run(["명령어"],"세부사항")등을 입력해서 터미널 명령어를 실행시킬 수 있습니다. 모듈에 대한 자세한 내용은 파이썬 공식 홈페이지를 참고하면 되겠습니다.
참고 : https://docs.python.org/3/library/subprocess.html
threading 모듈
스레드가 여러 개로 분리되면, 스레드 간 데이터 공유의 복잡성이 증가합니다. 또한 락과 데드락을 회피하는 데 주의를 기울여야 합니다. 파이썬 프로그램에는 단 하나의 메인 스레드만 존재합니다. 멀티 스레드를 사용하려면 threading 모듈을 사용합니다.
내부적으로 락을 관리하려면 queue 모듈을 사용합니다. 큐에 의존하면 자원의 접근을 직렬화할 수 있고, 이것은 곧 한 번에 하나의 스레드만 데이터에 접근할 수 있게 한다는 뜻입니다(FIFO 방식). 실행중인 스레드가 있는 동안에는 프로그램이 종료되지 않습니다.
⭐️ 락 : Lock란 자원을 사용하고 있을때 해당 자원에 대한 접근을 막기 위한 동기화 매커니즘 입니다.
⭐️ 데드락 : 데드락이란 교착상태라고도 불리며 한정된 자원을 여러 곳에서 사용하려고 할 때 발생할 수 있습니다. 데드락 발생 조건은 총 4가지가 있습니다.
- 상호배제 : 자원은 한 번에 한 프로세스만 사용 가능
- 점유 대기 : 최소한 하나의 자원을 점유하고 있으면서 다른 프로세스에 할당되어 사용하고 있는 자원을 추가로 점유하기 위해 대기하는 프로세스가 있어야한다
- 비선점 : 다른 프로세스에 할당된 자원은 사용이 끝날 때 까지 강제로 빼앗을 수 없다
- 순환 대기 : 프로세스의 집합에서 여러 자원들이 순환으로 대기할 때 발생(프로세스 ABC가 있으면 A는 B가 점유한 자원을 B는 C가 점유한 자원을 C는 A가 점유한 자원을 대기하는 상태이다)
예시코드
워커 스레드(warker thread)가 작업을 완료했는데도, 프로그램이 종료되지 않고 계속 실행되는 경우 문제가 발생될 수 있습니다. 스레드를 데몬으로 변환하면 데몬 스레드가 실행되지 않는 즉시 프로그램이 종료됩니다. queue.join()메서드는 큐가 빌 때 까지(큐의 모든 항목이 처리될 때 까지) 기다립니다. queue모듈을 수정한 코드는 다음과 같습니다.
import queue
import threading
q = queue.Queue()
def worker(num):
while True:
item = q.get()
if item is None:
break
# 작업을 처리합니다.
print(f"스레드 {num+1} : 처리 완료 {item}")
q.task_done()
if __name__ == "__main__":
worker_thread = 5
threads = []
for i in range(worker_thread):
t = threading.Thread(target=worker, args=(i,))
t.start()
threads.append(t)
for item in range(20):
q.put(item)
# 모든 작업이 끝날 까지 대기합니다(block)
q.join()
# 워커 스레드를 종료합니다
for i in range(worker_thread):
q.put(None)
for t in threads:
t.join()
이 코드는 작업의 양을 20개로 지정한 다음 스레드를 5개로 만든 후 작업을 처리하는 코드 입니다.
출력값은 다음과 같이 나오는 것을 확인할 수 있습니다. 출력 값은 매번 실행할 때마다 다를 수 있습니다.
스레드 1 : 처리 완료 0
스레드 1 : 처리 완료 1
스레드 1 : 처리 완료 2
스레드 1 : 처리 완료 3
스레드 1 : 처리 완료 4
스레드 1 : 처리 완료 5
스레드 1 : 처리 완료 6
스레드 1 : 처리 완료 10
스레드 5 : 처리 완료 9
스레드 3 : 처리 완료 8
스레드 3 : 처리 완료 14
스레드 1 : 처리 완료 12
스레드 2 : 처리 완료 11
스레드 3 : 처리 완료 15
스레드 1 : 처리 완료 16
스레드 2 : 처리 완료 17
스레드 4 : 처리 완료 7
스레드 5 : 처리 완료 13
스레드 3 : 처리 완료 18
스레드 1 : 처리 완료 19
뮤텍스와 세마포어
뮤텍스(mutex)
뮤텍스란 Lock와 같은 기술로 임계영역을 가진 스레드들의 실행 시간이 서로 겹치지 않게 각각 단독으로 실행되게 하는 기술입니다. 또한 공유 리소스에 한 번에 하나의 스레드만 접근 할 수 있도록 하는 상호 배제 동시성 제어 정책을 강제하기 위해 설계 되었습니다.
예를 들면 한 스레드가 배열을 수정하고 있다고 가정해보면, 배열 작업을 절반 이상 수행했을 때, 프로세서가 다른 스레드로 전환했다고 하면, 뮤텍스(mutex)를 사용하지 않는다면 두 스레드가 동시에 배열을 수정하는 일이 벌어질 것입니다.
이제 뮤텍스를 구현해 볼건데 뮤텍스는 개념적으로 1부터 시작하는 정수입니다. 스레드는 배열을 변경해야 할 때 까지 뮤텍스를 'Lock(잠군다)' 즉. 스레드는 뮤텍스가 양수가 될 때까지 대기한 다음 숫자를 1감소시킵니다. 이것을 다른말로 Lock이라고 합니다. 또한 마찬가지로 배열 수정을 마치면 뮤텍스가 잠금 해제되어 숫자가 1증가합니다. 이것을 Unlock(언락)이라고 합니다.
따라서 다른것이 작업하고 있을때는 동시에 수정하는 일어나지 않습니다. 이제 뮤텍스를 구현한 코드를 보겠습니다.
<mutex 동작 X일 경우>from threading import Thread, Lock
import threading
def worker(mutex, data, thread_safe):
if thread_safe:
mutex.acquire()
try:
print(f"스레드 {threading.get_ident()} : {data}\n")
finally:
if thread_safe:
mutex.release()
if __name__ == "__main__":
threads = []
thread_safe = False # mutex동작을 안한 상태
# thread_safe = True # mutex동작을 하는 상태
mutex = Lock()
for i in range(20):
t = Thread(target=worker, args=(mutex, i, thread_safe))
t.start()
threads.append(t)
for t in threads:
t.join()
<출력 값>
스레드 123145502261248 : 0
스레드 123145502261248 : 1
스레드 123145519050752 : 2
스레드 123145535840256 : 3
스레드 123145502261248 : 4
스레드 123145519050752 : 6
스레드 123145502261248 : 7
스레드 123145519050752 : 9
스레드 123145569419264 : 10
스레드 123145552629760 : 5
스레드 123145519050752 : 12
스레드 123145602998272 : 13
스레드 123145586208768 : 11
스레드 123145619787776 : 16
스레드 123145636577280 : 18
스레드 123145502261248 : 15
스레드 123145519050752 : 19
스레드 123145535840256 : 8
스레드 123145552629760 : 14
스레드 123145602998272 : 17
<mutex를 실행 시킨경우-출력값>
스레드 123145581740032 : 0
스레드 123145581740032 : 1
스레드 123145598529536 : 2
스레드 123145581740032 : 3
스레드 123145615319040 : 4
스레드 123145632108544 : 5
스레드 123145648898048 : 6
스레드 123145665687552 : 7
스레드 123145581740032 : 8
스레드 123145598529536 : 9
스레드 123145615319040 : 10
스레드 123145581740032 : 11
스레드 123145632108544 : 12
스레드 123145598529536 : 13
스레드 123145615319040 : 14
스레드 123145648898048 : 15
스레드 123145665687552 : 16
스레드 123145581740032 : 17
스레드 123145632108544 : 18
스레드 123145598529536 : 19
세마포어(semaphore)
세마포어는 뮤텍스보다 더 일반적으로 사용되는 개념입니다. 세마포어는 1보다 큰 수로 시작할 수 있습니다. 세마포어 값은 곧 한번에 자원에 접근할 수 있는 스레드의 수입니다. 세마포어는 뮤텍스의 Lock 과 Unlock 작업과 유사한 대기 및 신호 작업을 지원합니다. 이제 세마포어 예시를 보겠습니다.
import threading
import time
class ThreadPool(object):
def __init__(self):
self.active = []
self.lock = threading.Lock()
def acquire(self, name):
with self.lock:
self.active.append(name)
print(f"획득 : {name} | 스레드 풀 : {self.active}")
def release(self, name):
with self.lock:
self.active.remove(name)
print(f"반환 {name} | 스레드 풀: {self.active}")
def worker(semaphore, pool):
with semaphore:
name = threading.currentThread().getName()
pool.acquire(name)
time.sleep(1)
pool.release(name)
if __name__ == "__main__":
threads = []
pool = ThreadPool()
semaphore = threading.Semaphore(3)
for i in range(10):
t = threading.Thread(
target=worker, name="스레드 " + str(i), args=(semaphore, pool)
)
t.start()
threads.append(t)
for t in threads:
t.join()
<세마포어 출력 값>
획득 : 스레드 0 | 스레드 풀 : ['스레드 0']
획득 : 스레드 1 | 스레드 풀 : ['스레드 0', '스레드 1']
획득 : 스레드 2 | 스레드 풀 : ['스레드 0', '스레드 1', '스레드 2']
반환 스레드 0 | 스레드 풀: ['스레드 1', '스레드 2']
획득 : 스레드 3 | 스레드 풀 : ['스레드 1', '스레드 2', '스레드 3']
반환 스레드 2 | 스레드 풀: ['스레드 1', '스레드 3']
반환 스레드 1 | 스레드 풀: ['스레드 3']
획득 : 스레드 4 | 스레드 풀 : ['스레드 3', '스레드 4']
획득 : 스레드 5 | 스레드 풀 : ['스레드 3', '스레드 4', '스레드 5']
반환 스레드 3 | 스레드 풀: ['스레드 4', '스레드 5']
반환 스레드 5 | 스레드 풀: ['스레드 4']
반환 스레드 4 | 스레드 풀: []
획득 : 스레드 6 | 스레드 풀 : ['스레드 6']
획득 : 스레드 7 | 스레드 풀 : ['스레드 6', '스레드 7']
획득 : 스레드 8 | 스레드 풀 : ['스레드 6', '스레드 7', '스레드 8']
반환 스레드 6 | 스레드 풀: ['스레드 7', '스레드 8']
반환 스레드 7 | 스레드 풀: ['스레드 8']
반환 스레드 8 | 스레드 풀: []
획득 : 스레드 9 | 스레드 풀 : ['스레드 9']
반환 스레드 9 | 스레드 풀: []
참고 자료 및 서적
파이썬 자료구조와 알고리즘 : https://books.google.co.kr/books/about/파이썬_자료구조와_알고리즘.html?id=L12nDwAAQBAJ&printsec=frontcover&source=kp_read_button&redir_esc=y#v=onepage&q&f=false
세마 포어: ko.wikipedia.org/wiki/%EC%84%B8%EB%A7%88%ED%8F%AC%EC%96%B4
프로세스와 스레드 : velog.io/@yewon-july/Process-vs-Thread
'Python > Python 개념' 카테고리의 다른 글
UPbit API 이용해서 Python 연결해서 현재 계좌 조회하기 (0) | 2021.04.20 |
---|---|
파이썬을 이용해 영화 대본 원하는 캐릭터 대사 긁어모으기 (0) | 2021.04.05 |
파이썬 디자인 패턴 - 1 (2) | 2020.12.09 |
파이썬 클래스와 객체 (0) | 2020.12.09 |
파이썬 파일 처리하기 (0) | 2020.12.07 |
댓글