1. 멀티스레드 이론
1.1 스레드 정의
운영체제에서 각 프로세스는 독립적인 메모리 공간을 갖는다. 기본적으로 모든 프로세스는 하나의 제어 스레드(主线程)를 포함한다.
프로세스는 자원 집합으로 간주되며, 스레드는 CPU에서 실행되는 단위이다. 멀티스레드는 하나의 프로세스 내에 여러 제어 스레드가 존재하는 개념으로, 해당 프로세스의 주소 공간을 공유한다.
예시: 상해 지하철과 광저우 지하철은 별개의 프로세스이지만, 광저우 지하철 13号线는 하나의 스레드로, 모든 선로는 공유된 자원을 사용한다.
1.2 스레드 생성 비용
프로세스 생성 시 메모리 주소 공간을 할당해야 하지만, 스레드 생성은 프로세스 내에서 이루어져 비용이 적다.
1.3 프로세스/스레드 관계
다른 프로세스는 경쟁 관계(예: 네트워크 대역폭 분쟁), 같은 프로세스 내 스레드는 협력 관계이다.
1.4 스레드 vs 프로세스 차이점
- 스레드: 공유 주소 공간, 데이터 섹션 직접 접근, 다른 스레드와의 간접 통신
- 프로세스: 독립적 주소 공간, 부모 프로세스 데이터 복사본, IPC(인터프로세스 통신) 필요
- 스레드 생성: 쉽고, 프로세스 생성: 부모 프로세스 복사 필요
- 스레드 제어권: 동일한 프로세스 내 스레드에 대해 높은 권한, 프로세스는 자식 프로세스만 제어 가능
- 메인 스레드 변경: 다른 스레드에 영향, 부모 프로세스 변경: 자식 프로세스 영향 없음
1.5 순차/병렬 실행
비join 방식: 자식 프로세스 start 후 바로 join → 순차 실행
모든 자식 프로세스 start 후 차례로 join → 병렬 실행
2. 스레드 생성 방법
2.1 Thread 함수 직접 호출
import threading
import time
import random
def execute_task(name):
print(f'작업 스레드{name} 시작')
delay = random.randint(1, 3)
time.sleep(delay)
print(f'작업 스레드{name} {delay}초 후 종료')
def initiate_threads():
thread_list = [threading.Thread(target=execute_task, args=(i,)) for i in range(1, 4)]
[t.start() for t in thread_list]
[t.join() for t in thread_list]
if __name__ == '__main__':
print('메인 프로세스 시작')
initiate_threads()
print('메인 프로세스 종료')
2.2 Thread 클래스 상속
import threading
import time
import random
class CustomThread(threading.Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
print(f'작업 스레드{self.name} 시작')
delay = random.randint(1, 3)
time.sleep(delay)
print(f'작업 스레드{self.name} {delay}초 후 종료')
def initiate_threads():
thread_list = [CustomThread(name=f'{i}') for i in range(1, 4)]
[t.start() for t in thread_list]
[t.join() for t in thread_list]
if __name__ == '__main__':
print('메인 프로세스 시작')
initiate_threads()
print('메인 프로세스 종료')
2.3 join() 함수 차이점
서브스레드 join 미호출 시:
sleep 없음: 메인 프로세스가 서브스레드 기다리지 않음
sleep 있음: 메인 프로세스가 서브스레드 기다리지 않아 실행 순서 차이 발생
3. 스레드 속성
현재 스레드 이름 확인: threading.current_thread().name
활성 스레드 수 확인: threading.active_count()
import time
import os
import threading
def monitor_task():
print(f'스레드{threading.current_thread().name} 실행 중, PID:{os.getpid()}, 활성 스레드:{threading.active_count()}')
time.sleep(0.01)
print(f'스레드{threading.current_thread().name} 종료, PID:{os.getpid()}, 활성 스레드:{threading.active_count()}')
def initiate_threads():
thread_list = [threading.Thread(target=monitor_task) for _ in range(3)]
[t.start() for t in thread_list]
if __name__ == '__main__':
print('메인 프로세스 시작')
initiate_threads()
print('메인 프로세스 종료')
4. 멀티스레드 TCP 서버 구현
# 서버
import socket
import threading
def handle_client(conn, addr):
while True:
data = conn.recv(1024)
print(f'클라이언트 {addr} 데이터: {data.decode("utf-8")}')
conn.send(data.decode("utf-8").upper().encode('utf-8'))
def start_server():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('127.0.0.1', 9002))
server.listen(3)
while True:
conn, addr = server.accept()
threading.Thread(target=handle_client, args=(conn, addr)).start()
if __name__ == '__main__':
start_server()
# 클라이언트
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 9002))
while True:
msg = input('전송할 메시지: ')
client.send(msg.encode('utf-8'))
response = client.recv(1024)
print(f'서버 응답: {response.decode("utf-8")}')
5. 뮤텍스 락
5.1 이론
공유 자원 접근을 제어하는 메커니즘. 동시에 하나의 스레드만 자원을 접근 가능하게 하여 데이터 불일치를 방지.
자원 접근 전 락 획득, 작업 완료 후 해제. 이 과정으로 코드 세그먼트의 안정성을 보장.
5.2 스레드 락 예제
import time
import threading
def update_value(name, lock):
lock.acquire()
print(f'스레드{name} 실행')
time.sleep(1)
print(f'스레드{name} 종료')
lock.release()
def initiate_threads():
lock = threading.Lock()
threads = [threading.Thread(target=update_value, args=(i, lock)) for i in range(3)]
[t.start() for t in threads]
[t.join() for t in threads]
if __name__ == '__main__':
initiate_threads()
5.3 다중 프로세스 티켓 예제
락 추가 전:
import json
import os
import time
from multiprocessing import Process
def buy_ticket(username):
ticket_dict = read_ticket()
print(f'사용자{username} 티켓 확인: {ticket_dict["ticket_num"]}')
time.sleep(0.1)
if ticket_dict["ticket_num"] > 0:
ticket_dict["ticket_num"] -= 1
save_ticket(ticket_dict)
print(f'사용자{username} 성공: {ticket_dict["ticket_num"]}')
else:
print('티켓 부족')
def create_processes():
processes = [Process(target=buy_ticket, args=(f'{i}',)) for i in range(3)]
[p.start() for p in processes]
[p.join() for p in processes]
락 추가 후:
import json
import os
import time
from multiprocessing import Process, Lock
def buy_ticket(username, lock):
ticket_dict = read_ticket()
print(f'사용자{username} 티켓 확인: {ticket_dict["ticket_num"]}')
time.sleep(0.1)
lock.acquire()
ticket_dict = read_ticket()
if ticket_dict["ticket_num"] > 0:
ticket_dict["ticket_num"] -= 1
save_ticket(ticket_dict)
print(f'사용자{username} 성공')
else:
print('티켓 부족')
lock.release()
6. 스레드 뮤텍스 락
스레드는 공유 메모리 공간을 사용하므로 락 필수. 프로세스는 독립 메모리 공간으로 락 필요 없음.
| 특징 | 스레드 | 프로세스 |
|---|---|---|
| 메모리 모델 | 공유 변수/힙 | 독립 공간(쓰기 시 복사) |
| 통신 비용 | 낮음(직접 변수 접근) | 높음(Queue/Pipe 필요) |
| 데이터 동기화 | Lock/세마포어 필요 | 자연스럽게 격리 |
| GIL 제약 | 있음(CPython) | 없음 |
7. 비락 상태 실행 순서
스레드:
import time
import threading
money = 99
def task1():
global money
temp = money
print(f'task1 sleep 전: {money}')
time.sleep(0.01)
print(f'task1 수정 전: {money}')
money = temp - 1
print(f'task1 수정 후: {money}')
def task2():
global money
temp = money
print(f'task2 수정 전: {money}')
money = temp + 2
print(f'task2 수정 후: {money}')
def initiate_threads():
thread1 = threading.Thread(target=task1)
thread2 = threading.Thread(target=task2)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(f'최종 금액: {money}')
프로세스:
import time
from multiprocessing import Process
money = 99
def task1():
global money
temp = money
print(f'task1 sleep 전: {money}')
time.sleep(0.01)
print(f'task1 수정 전: {money}')
money = temp - 1
print(f'task1 수정 후: {money}')
def task2():
global money
temp = money
print(f'task2 수정 전: {money}')
money = temp + 2
print(f'task2 수정 후: {money}')
def initiate_processes():
process1 = Process(target=task1)
process2 = Process(target=task2)
process1.start()
process2.start()
process1.join()
process2.join()
print(f'최종 금액: {money}')
8. 핵심 개념 추가
스레드의 sleep과 락은 다른 스레드로 전환 가능. 락이 먼저 실행되면 스레드 전환도 가능.
락 위치에 따라 실행 순서 달라짐. 락 앞에 코드가 있으면 해당 코드 실행 후 락 처리.
9. 추가 지식
print() 함수는 원자적 연산이 아니므로 출력 순서 혼란 발생. GIL과 I/O 동기화 문제로 인한 출력 순서 불확실성.