고급 웹프로그래밍
운영체제와 파이썬 기본 개념
rabbit-jun
2025. 4. 3. 11:29
비유로 보는 OS
먼저 비유를 읽으며 느낌을 알고 아래 글을 읽은 뒤에 다시 비유를 보면 더 편하게 이해가 될 듯 하다
OS: 농장주, python: 관리자, CPU: 노예, 프로세스: 농사
- 멀티코어는 노예가 손이 여러 개라서 한 팔로 밭을 갈고, 다른 팔로 추수를 동시에 함.
- 멀티프로세싱은 노예 여러명 혹은 팔 여러 달린 노예한테 동시에 밭 갈고, 추수하라고 시키는 것
- 멀티스레딩은 한 작업 안에서 왼손은 작물을 따고, 오른손은 바구니에 담음.
- GIL은 관리자가 “왼손이 따고 있는데 오른손이 갑자기 바구니 뒤적이면 위험하니까 한 손만 써!” 라고 함.
- 비동기는 밭을 갈다가 농기구가 고장 나면 “수리 기다릴 동안 시간 아까우니 추수하러 감”(빠르게 전환하면 동시에 하는 것처럼 보임)
- 콜백은 “추수 끝나면 밭 다시 가시오” 라고 쪽지를 붙여놓는 것
- 콜백이 많아지면 쪽지 위에 쪽지, 위에 쪽지… → 노예 혼란 (콜백 지옥)
동시성
- 동시에 실행되는 것처럼 행동하는 것
- 병렬 컴퓨팅(parallel computing): 하나의 작업을 쪼개서 여러 CPU 또는 여러 코어에서 진짜 동시에 처리
- 동시 컴퓨팅(concurrent computing): 여러 작업을 동시에 진행 중처럼 보이게 처리함 → CPU는 한 번에 하나씩 처리하지만 빠르게 전환 (컨텍스트 스위칭)
- 분산 컴퓨팅(distributed computing)은 서로 다른 물리적 머신에서 작업을 나눠 처리하고 네트워크로 통신
항목 | 병렬 컴퓨팅 | 분산 컴퓨팅 |
---|---|---|
정의 | 하나의 작업을 여러 CPU/코어로 동시에 처리 | 여러 컴퓨터(노드)가 협력해 작업을 나눠 처리 |
시스템 구조 | 하나의 시스템 안에 여러 코어/CPU | 여러 시스템(노드/머신/서버)로 구성 |
메모리 구조 | 공유 메모리 (shared Memory) 가능 | 분산 메모리( Distributed Memory) |
위치 | 하나의 머신(멀티코어 서버) | 서로 다른 위치의 여러 머신 |
목적 | 속도 향상(시간 절약) | 자원 확장성,내결함성,비용 효율 |
예시 기술 | OpenMP, CUDA, multiprocessing | Hadop, Spark, Kubernetes,gRPC |
데이터 처리 | 단일 머신 안에서 분산됨 | 네트워크 통해 통신하며 작업 |
프로세스(Process)
- 프로세스란 운영체제가 관리하는 실행 중인 프로그램의 인스턴스
- 메모리 + 자원 + 코드 + 데이터 + 스택을 가진 독립된 작업 단위
- 프로세스는 독립적인 메모리 공간(코드 영역, 데이터 영역, 힙, 스택),다른 프로세스와 메모리 공간을 공유하지 않음
- 프로세스 간 통신(Inter Process Communication)은 복잡
- OS스케줄러가 각 프로세스를 독립적으로 관리
- 생성 비용 큼, 컨텍스트 스위칭 비용 큼
컨텍스트 스위칭: CPU/코어에서 실행 중이던 프로세스/스레드가 다른 프로세스/스레드로 교체되는 것
멀티 프로세싱
- 멀티프로세싱(Multi-processing)은 하나의 프로그램이 여러 개의 프로세스(Process)를 생성하여 병렬로 실행.
- 각각 독립된 메모리 공간을 가지므로 GIL의 영향을 받지 않음
- 멀티코어 CPU를 활용해 진짜 병렬 실행이 가능하지만, IPC가 필요함 (데이터 공유가 어려움)
- IPC = Inter Process Communication( 프로세스 간 통신)
- 멀티프로세싱은 각 프로세스가 메모리를 독립적으로 갖기 때문에 서로 직접적으로 데이터 공유 못함
- 파이썬의
multiprocessing
은 OS-Level프로세스 생성.- 스케줄링은 전적으로 OS가 담당
스케줄링이란?
- CPU를 어떤 작업(프로세스나 스레드)에게 언제, 얼마나 오래 할당할지 결정하는 것
멀티 프로세싱 예시
import time
from multiprocessing import Process
def say_hello(name):
print(f"[{name}] 시작")
time.sleep(2)
print(f"[{name}] 끝")
if __name__ == "__main__":
start = time.time()
for i in range(3):
p = Process(target=say_hello, args=(i,)) # 함수에 인자를 넣어줄 때 튜플의 방식을 취해야 한다
p.start() # 프로세스 시작
print("모든 프로세스 실행 중...")
print(f"총 소요 시간: {time.time() - start:.2f}초")
여러 프로세스를 동시에 실행하기 때문에 하나씩 순서대로 처리하는 코드보다 훨씬 빠름(아래의 코드와 비교하면 그 차이를 느낄 수 있다)
순차적 실행
import time
def say_hello(name):
print(f"[{name}] 시작")
time.sleep(2)
print(f"[{name}] 끝")
if __name__ == "__main__":
start = time.time()
for i in range(3):
say_hello(i)
end = time.time()
print(f"총 소요 시간: {end - start:.2f}초")
Global Interpreter Lock
- 파이썬(CPython)의 스레드 동시 실행 제한 장치
- 한 번에 오직 하나의 스레드만 실행 가능
- 멀티스레드는 동시 실행처럼 보이지만 사실은 컨텍스트 스위칭
multiprocessing
은 CPU-bound에 사용하고.threading
은 I/o-bound에 사용함- 파이썬 3.13부터 GIL을 제거할 수 있음 (공식적으로
Free-threaded Python
모드 등장)
CPU Bound: 프로세스가 진행될 때, CPU 사용 기간이 I/O Wait보다 많은 경우
I/O Wait: CPU가 입출력을 대기하는데 사용한 시간의 비율, 프로세스에 바로 접근 할 수 없는 상황인 경우 I/O Wait 비율이 높음
운영 체제 스레드
1. 스레드(thread)
- 스레드는 프로세스 내부에서 실행되는 흐름의 단위로 하나의 프로세스 안에서 여러 작업을 동시에 실행할 수 있게 해줌
2. 멀티 스레딩(multi-threading)
- 멀티 스레딩은 하나의 프로세스 내에서 여러 개의 스레드를 생성하여 실행하는 방식
- 메모리를 공유하지만, GIL때문에 Python에서는 CPU연산에 비효율적
- 멀티 스레딩은 공유 메모리로 데이터 전달이 빠르고, 스레드 생성/종료 비용이 낮음
- 동기화 문제가 발생할 수 있고, 하나의 스레드가 죽으면 전체 프로세스에 영향을 미침
- 스케줄링은 OS 커널이 담당하고 파이썬은 언제 작업을 실행할지 정하지 않음
단일Vs멀티스레딩Vs멀티프로세싱 cpu-bound예시
import time
from threading import Thread
from multiprocessing import Process
def count():
total = 0
for i in range(100_000_000):
total += i
return total
def run_single():
print("\n 그냥 연산 시작")
start = time.time()
count()
end = time.time()
print(f"연산 총 시간: {end - start:.2f}초")
def run_threads():
print("\n 멀티스레딩 시작")
start = time.time()
t1 = Thread(target=count)
t2 = Thread(target=count)
t1.start()
t2.start()
t1.join()
t2.join()
end = time.time()
print(f" 멀티스레딩 총 시간: {end - start:.2f}초")
def run_processes():
print("\n 멀티프로세싱 시작")
start = time.time()
p1 = Process(target=count)
p2 = Process(target=count)
p1.start()
p2.start()
p1.join()
p2.join()
end = time.time()
print(f"멀티프로세싱 총 시간: {end - start:.2f}초")
if __name__ == "__main__":
run_single()
run_threads()
run_processes()
결과를 보면 단일 연산이 멀티스레딩보다 빠름 -> python에서 GIL에 의해 병렬 처리도 못하는데 문맥전환하는 비용은 그대로 들기 때문
단일Vs멀티스레딩Vs멀티프로세싱 I/O-bound예시
import time
from threading import Thread
from multiprocessing import Process
def io_task(name):
print(f"{name} 시작")
time.sleep(1)
print(f"{name} 완료")
def run_single():
print("\n 단일 실행 시작")
start = time.time()
for i in range(5):
io_task(f"작업 {i}")
end = time.time()
print(f" 단일 실행 총 시간: {end - start:.2f}초")
def run_threads():
print("\n 멀티스레딩 시작")
start = time.time()
threads = []
for i in range(5):
t = Thread(target=io_task, args=(f"스레드 {i}",))
t.start()
threads.append(t)
for t in threads:
t.join()
end = time.time()
print(f" 멀티스레딩 총 시간: {end - start:.2f}초")
def run_processes():
print("\n 멀티프로세싱 시작")
start = time.time()
processes = []
for i in range(5):
p = Process(target=io_task, args=(f"프로세스 {i}",))
p.start()
processes.append(p)
for p in processes:
p.join()
end = time.time()
print(f" 멀티프로세싱 총 시간: {end - start:.2f}초")
if __name__ == "__main__":
run_single()
run_threads()
run_processes()
시간 오래 걸리는 I/O 작업(예: sleep, 파일 읽기, 네트워크 요청 등) 중에는 GIL이 잠깐 해지되므로 멀티스레드가 더 빠름
그린스레드
- OS가 아닌 유저 레벨에서 스케줄링되는 스레드로 가짜 스레드/가벼운 스레드라 할 수 있음
- OS에게 보이지 않고, 문맥 전환이 매우 가볍고 빠름
- 실제 CPU 병렬성은 없음
예시
from greenlet import greenlet
def task1():
print("Task 1: Start")
g2.switch() # task2로 전환
print("Task 1: Resume")
def task2():
print("Task 2: Start")
g1.switch() # task1로 전환
print("Task 2: Resume")
g1 = greenlet(task1)
g2 = greenlet(task2)
g1.switch()
- greenlet(f) : f 함수를 실행할 수 있는 작은 실행 단위
- switch() : 다른 greenlet으로 제어권 전환 ( 즉시 실행)
콜백
- 함수를 인자로 전달해서, 나중에 실행되는 함수
- 복잡한 흐름이 많아지면 콜백이 여기저기서 엄청 쓰이는 그야말로 콜백 지옥이 펼쳐짐
에시
def long_task(callback):
result = "작업 완료"
callback(result)
def when_done(data):
print(f"콜백 받은 결과: {data}")
long_task(when_done)
파이썬 제너레이터
- 원하는 곳에서 멈추고 어느 곳에서든
return
을 하거나yield
키워드를 사용해 다시 그 지점으로 돌아갈 수 있음yield
키워드를 통해 함수 상태를 일시 중단 또는 재개할 수 있는 이터레이터
예시
def counter():
yield 1
yield 2
yield 3
gen = counter()
for number in gen:
print(f"제너레이터에서 받은 값: {number}")
반응형