고급 웹프로그래밍

운영체제와 파이썬 기본 개념

rabbit-jun 2025. 4. 3. 11:29

비유로 보는 OS

먼저 비유를 읽으며 느낌을 알고 아래 글을 읽은 뒤에 다시 비유를 보면 더 편하게 이해가 될 듯 하다

 

OS: 농장주, python: 관리자, CPU: 노예, 프로세스: 농사

  • 멀티코어는 노예가 손이 여러 개라서 한 팔로 밭을 갈고, 다른 팔로 추수를 동시에 함.
  • 멀티프로세싱은 노예 여러명 혹은  팔 여러 달린 노예한테 동시에 밭 갈고, 추수하라고 시키는 것
  • 멀티스레딩은 한 작업 안에서 왼손은 작물을 따고, 오른손은 바구니에 담음.
  • GIL은 관리자가  “왼손이 따고 있는데 오른손이 갑자기 바구니 뒤적이면 위험하니까 한 손만 써!” 라고 함.
  • 비동기는 밭을 갈다가 농기구가 고장 나면 “수리 기다릴 동안 시간 아까우니 추수하러 감”(빠르게 전환하면 동시에 하는 것처럼 보임)
  • 콜백은 “추수 끝나면 밭 다시 가시오” 라고 쪽지를 붙여놓는 것
  • 콜백이 많아지면 쪽지 위에 쪽지, 위에 쪽지… → 노예 혼란 (콜백 지옥)

 

동시성

  • 동시에 실행되는 것처럼 행동하는 것
  1. 병렬 컴퓨팅(parallel computing): 하나의 작업을 쪼개서 여러 CPU 또는 여러 코어에서 진짜 동시에 처리
  2. 동시 컴퓨팅(concurrent computing): 여러 작업을 동시에 진행 중처럼 보이게 처리함 → CPU는 한 번에 하나씩 처리하지만 빠르게 전환 (컨텍스트 스위칭)
  3. 분산 컴퓨팅(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( 프로세스 간 통신)
    • 멀티프로세싱은 각 프로세스가 메모리를 독립적으로 갖기 때문에 서로 직접적으로 데이터 공유 못함
  • 파이썬의 multiprocessingOS-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}")
반응형