TL;DR

  • Python์˜ ๋™์‹œ์„ฑ/๋ณ‘๋ ฌ์„ฑ์€ thread์™€ process ๋‘ ๊ฐˆ๋ž˜์ด๋ฉฐ GIL ๋•Œ๋ฌธ์— CPU ๋ณ‘๋ ฌ์€ process์˜ ๋ชซ
  • threading์€ I/O-bound์— ์ ํ•ฉํ•œ ์ €์ˆ˜์ค€, multiprocessing์€ GIL์„ ์šฐํšŒํ•ด CPU-bound์— ์ ํ•ฉ, concurrent.futures๋Š” ๋‘˜์„ ๊ฐ์‹ผ ๊ณ ์ˆ˜์ค€
  • ์„ ํƒ ๊ธฐ์ค€์€ I/O-bound๋Š” thread, CPU-bound๋Š” process, ๋Œ€๊ทœ๋ชจ I/O๋Š” asyncio

AI-assisted


1. ๋‘ ๊ฐˆ๋ž˜ โ€” thread์™€ process

Python์—์„œ ์—ฌ๋Ÿฌ ์ผ์„ ๊ฒน์น˜๊ฑฐ๋‚˜ ๋‚˜๋ˆ  ๋Œ๋ฆฌ๋Š” ๊ธธ์€ ํฌ๊ฒŒ ๋‘˜์ด๋‹ค.

  • thread: ํ•œ process ์•ˆ์˜ ์—ฌ๋Ÿฌ ์‹คํ–‰ ํ๋ฆ„. ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ๊ณต์œ ํ•œ๋‹ค. ์ƒ์„ฑ์ด ๊ฐ€๋ณ๋‹ค.
  • process: ๋…๋ฆฝ๋œ ํ”„๋กœ๊ทธ๋žจ ์ธ์Šคํ„ด์Šค. ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ๊ฒฉ๋ฆฌ๋œ๋‹ค. ์—ฌ๋Ÿฌ ์ฝ”์–ด์—์„œ ์ง„์งœ ๋ณ‘๋ ฌ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

๋ฌด์—‡์„ ๊ณ ๋ฅผ์ง€๋Š” GIL(Global Interpreter Lock)์ด ๊ฐ€๋ฅธ๋‹ค.

CPython์—๋Š” ํ•œ ์ˆœ๊ฐ„์— ํ•˜๋‚˜์˜ thread๋งŒ Python ๋ฐ”์ดํŠธ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜๊ฒŒ ๋ง‰๋Š” ์ž ๊ธˆ์ด ์žˆ์–ด์„œ thread๋ฅผ ์—ฌ๋Ÿฌ ๊ฐœ ๋„์›Œ๋„ ์ˆœ์ˆ˜ Python ๊ณ„์‚ฐ์€ ํ•œ ๋ฒˆ์— ํ•˜๋‚˜์”ฉ๋งŒ ๋ˆ๋‹ค. ๊ทธ๋ž˜์„œ CPU-bound ์ž‘์—…์€ thread๋กœ ๋‚˜๋ˆ ๋„ ๋นจ๋ผ์ง€์ง€ ์•Š๊ณ  ์—ฌ๋Ÿฌ ์ฝ”์–ด๋ฅผ ์‹ค์ œ๋กœ ์“ฐ๋ ค๋ฉด GIL์ด process๋งˆ๋‹ค ๋”ฐ๋กœ ์žˆ๋Š” multiprocessing์œผ๋กœ ๊ฐ€์•ผ ํ•œ๋‹ค.

๋ฐ˜๋Œ€๋กœ I/O ๋Œ€๊ธฐ ์ค‘์—๋Š” GIL์ด ํ’€๋ฆฌ๋ฏ€๋กœ, I/O-bound ์ž‘์—…์—๋Š” thread๋„ ์ถฉ๋ถ„ํžˆ ์“ธ๋ชจ ์žˆ๋‹ค.

threadprocess
๋ฉ”๋ชจ๋ฆฌ๊ณต์œ (ํž™ยท์ „์—ญ)๋…๋ฆฝ(๊ฒฉ๋ฆฌ)
CPU ๋ณ‘๋ ฌGIL์ด ๋ง‰์Œ๊ฐ€๋Šฅ
์ƒ์„ฑยท์ „ํ™˜ ๋น„์šฉ๊ฐ€๋ฒผ์›€๋ฌด๊ฑฐ์›€(์ƒ์„ฑ + IPC)
ํ†ต์‹ ๊ณต์œ  ๋ฉ”๋ชจ๋ฆฌ๋กœ ๋ฐ”๋กœIPC ํ•„์š”(pickle ์ง๋ ฌํ™”)
์ ํ•ฉํ•œ ์ž‘์—…I/O-boundCPU-bound

2. threading โ€” OS thread ์ €์ˆ˜์ค€

threading์€ OS thread ๊ธฐ๋ฐ˜ ๋™์‹œ์„ฑ์„ ์ œ๊ณตํ•˜๋Š” ํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋‹ค. I/O-bound ์ž‘์—…์—์„œ ๋Œ€๊ธฐ๋ฅผ ๊ฒน์น˜๊ฑฐ๋‚˜, ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์›Œ์ปค๋ฅผ ๋Œ๋ฆด ๋•Œ ์“ด๋‹ค.

thread ์ƒ์„ฑ๊ณผ ์‹คํ–‰

import threading
 
def worker(name):
    print(f"{name} ์ž‘์—… ์ค‘")
 
t = threading.Thread(target=worker, args=("A",))
t.start()   # non-blocking: worker๋ฅผ ์ƒˆ thread์—์„œ ์‹œ์ž‘, ๋ฉ”์ธ์€ ์ฆ‰์‹œ ๋‹ค์Œ ์ค„๋กœ (async launch)
# ... ์ด ์‚ฌ์ด ์ฝ”๋“œ๋Š” worker์™€ ๊ฒน์ณ์„œ ๋ˆ๋‹ค (concurrent) ...
t.join()    # blocking + synchronous: worker๊ฐ€ ๋๋‚  ๋•Œ๊นŒ์ง€ ๋ฉ”์ธ์ด Blocked๋กœ ๋Œ€๊ธฐ (์™„๋ฃŒ๋ฅผ ์ง์ ‘ pull)
  • start(): ์ƒˆ thread๋ฅผ ๋งŒ๋“ค์–ด target ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•œ๋‹ค. non-blocking์ด๋ผ ์ฆ‰์‹œ ๋ฆฌํ„ดํ•˜๊ณ  worker๋Š” ๋’ค์—์„œ concurrentํ•˜๊ฒŒ ๋ˆ๋‹ค โ€” ์ž‘์—…๋งŒ ๊ฑธ์–ด๋‘๋Š” async launch๋‹ค. run()์„ ์ง์ ‘ ๋ถ€๋ฅด๋ฉด ์ƒˆ thread ์—†์ด ํ˜„์žฌ thread์—์„œ ์‹คํ–‰๋˜๋‹ˆ ์ฃผ์˜ํ•œ๋‹ค.
  • join(): ํ•ด๋‹น thread๊ฐ€ ๋๋‚  ๋•Œ๊นŒ์ง€ ํ˜ธ์ถœํ•œ ์ชฝ์ด ๊ธฐ๋‹ค๋ฆฐ๋‹ค. blocking + synchronous๋‹ค โ€” ํ˜ธ์ถœํ•œ thread๊ฐ€ Blocked ์ƒํƒœ๋กœ ์ž ๋“ค์–ด ์™„๋ฃŒ๋ฅผ ์ง์ ‘ ๊ธฐ๋‹ค๋ ค ํšŒ์ˆ˜ํ•œ๋‹ค(pull). ๋‹จ worker์˜ ๋ฆฌํ„ด๊ฐ’์€ ์ฃผ์ง€ ์•Š๊ณ  โ€œ๋๋‚ฌ๋‹คโ€๋Š” ์‹ ํ˜ธ๋งŒ ์ค€๋‹ค. ๊ฒฐ๊ณผ๊ฐ€ ํ•„์š”ํ•˜๋ฉด queue.Queue๋‚˜ 4์ ˆ์˜ Future๋ฅผ ์“ด๋‹ค.
  • daemon: ์œ„ ์ตœ์†Œ ์˜ˆ์‹œ์—” ์—†์ง€๋งŒ ์ƒ์„ฑ ์‹œ Thread(target=..., daemon=True)(๋˜๋Š” t.daemon = True)๋กœ ์ง€์ •ํ•˜๋Š” ์˜ต์…˜์ด๋‹ค. ์ด๋ ‡๊ฒŒ ๋งŒ๋“  thread๋Š” ๋ฉ”์ธ thread๊ฐ€ ๋๋‚˜๋ฉด ํ•จ๊ป˜ ๊ฐ•์ œ ์ข…๋ฃŒ๋œ๋‹ค. ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ƒ์ฃผ ์ž‘์—…์— ์“ฐ๋˜, ์ •๋ฆฌ ์—†์ด ์ฃฝ์œผ๋‹ˆ ์ค‘์š”ํ•œ ์ž‘์—…์—” ๋ถ€์ ํ•ฉํ•˜๋‹ค.

๋™๊ธฐํ™” primitives

์—ฌ๋Ÿฌ thread๊ฐ€ ๊ฐ™์€ ์ž์›์„ ๋™์‹œ์— ๊ฑด๋“œ๋ฆฌ๋ฉด race condition์ด ์ƒ๊ธด๋‹ค. ์ ‘๊ทผ ์ˆœ์„œ๋ฅผ ๊ฐ•์ œํ•˜๋Š” ๋„๊ตฌ๋“ค์ด๋‹ค.

  • Lock: ๊ฐ€์žฅ ๊ธฐ๋ณธ. ํ•œ ๋ฒˆ์— ํ•˜๋‚˜์˜ thread๋งŒ critical section์— ๋“ค์–ด๊ฐ€๊ฒŒ ํ•œ๋‹ค. ์†Œ์œ ๊ถŒ ๊ฐœ๋…์ด ์—†์–ด ์•„๋ฌด thread๋‚˜ releaseํ•  ์ˆ˜ ์žˆ๊ณ  ๊ฐ™์€ thread๊ฐ€ ํ’€์ง€ ์•Š๊ณ  ๋˜ acquireํ•˜๋ฉด ์ž๊ธฐ ์ž์‹ ์„ ๊ธฐ๋‹ค๋ฆฌ๋‹ค deadlock์— ๋น ์ง„๋‹ค(๋ฝ์„ ๋ชป ์žก์€ thread๋ฅผ ์žฌ์šฐ๋А๋ƒ ์Šคํ•€์‹œํ‚ค๋А๋ƒ์˜ ๊ฐˆ๋ฆผ์€ Busy Polling and Spinlock).
  • RLock: reentrant lock. ํš๋“ํ•œ thread๊ฐ€ ์†Œ์œ ๊ถŒ์„ ๊ฐ€์ง€๋ฉฐ ๊ทธ thread๋Š” ํ’€์ง€ ์•Š๊ณ ๋„ ์—ฌ๋Ÿฌ ๋ฒˆ ๋‹ค์‹œ acquireํ•  ์ˆ˜ ์žˆ๋‹ค(์žฌ์ง„์ž…). ํš๋“ ํšŸ์ˆ˜๋งŒํผ releaseํ•ด์•ผ ํ’€๋ฆฌ๊ณ  ์†Œ์œ ํ•œ thread๋งŒ releaseํ•  ์ˆ˜ ์žˆ๋‹ค. ์žฌ๊ท€ ํ•จ์ˆ˜๋‚˜, ๋ฝ์„ ์ฅ” ๋ฉ”์„œ๋“œ๊ฐ€ ๊ฐ™์€ ๋ฝ์„ ์“ฐ๋Š” ๋‹ค๋ฅธ ๋ฉ”์„œ๋“œ๋ฅผ ๋ถ€๋ฅผ ๋•Œ.
  • Event: thread ์‚ฌ์ด ์‹ ํ˜ธ ๊นƒ๋ฐœ. ํ•œ thread๊ฐ€ set()ํ•˜๋ฉด wait()ํ•˜๋˜ thread๋“ค์ด ๊นจ์–ด๋‚œ๋‹ค. โ€œ์ค€๋น„๋๋‹คโ€๋Š” ํ†ต์ง€์— ์“ด๋‹ค. ์ด๋ฆ„์€ ๋น„์Šทํ•ด๋„ asyncio์˜ event loop์™€๋Š” ๋ฌด๊ด€ํ•˜๋‹ค โ€” ๊ทธ๋ƒฅ boolean ํ”Œ๋ž˜๊ทธ๋‹ค.
  • Condition: ํŠน์ • ์กฐ๊ฑด์ด ๋  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐยทํ†ต์ง€. producer-consumer์˜ ์„ธ๋ฐ€ํ•œ ์ œ์–ด์—.
  • Semaphore: ๋™์‹œ ์ง„์ž… ์ˆ˜๋ฅผ N๊ฐœ๋กœ ์ œํ•œ. ์ปค๋„ฅ์…˜ ํ’€์ฒ˜๋Ÿผ โ€œ๋™์‹œ K๊ฐœ๊นŒ์ง€๋งŒโ€ ํ—ˆ์šฉํ•  ๋•Œ.

Lock์œผ๋กœ ๊ณต์œ  ๋ณ€์ˆ˜ ๋ณดํ˜ธ: ๊ฐ€์žฅ ๊ธฐ๋ณธ ํŒจํ„ด์ด๋‹ค.

lock = threading.Lock()
counter = 0
 
def increment():
    global counter
    with lock:        # ์ด ๋ธ”๋ก์„ ํ•œ thread์”ฉ ์ง๋ ฌํ™”
        counter += 1

Lock vs RLock โ€” ์†Œ์œ ๊ถŒ๊ณผ ์žฌ์ง„์ž…. ์•„๋ž˜๋Š” Lock์ด๋ฉด ๋‘ ๋ฒˆ์งธ ํš๋“์—์„œ ์ž๊ธฐ ์ž์‹ ์„ ๊ธฐ๋‹ค๋ฆฌ๋ฉฐ ๋ฉˆ์ถ˜๋‹ค(deadlock). RLock์€ ๊ฐ™์€ thread์˜ ์žฌํš๋“์„ ํ—ˆ์šฉํ•ด ํ†ต๊ณผํ•œ๋‹ค.

lock = threading.RLock()   # threading.Lock()์ด๋ฉด ์•„๋ž˜์—์„œ deadlock
 
def outer():
    with lock:
        inner()            # ๋ฝ์„ ์ฅ” ์ฑ„ ๋‹ค๋ฅธ ํ•จ์ˆ˜๋ฅผ ๋ถ€๋ฅธ๋‹ค
 
def inner():
    with lock:             # ๊ฐ™์€ thread๊ฐ€ ๋˜ acquire โ€” RLock์ด๋ผ ํ†ต๊ณผ
        ...

์†Œ์œ ๊ถŒ์œผ๋กœ ์ •๋ฆฌํ•˜๋ฉด, RLock์€ ์†Œ์œ ํ•œ thread๋งŒ ํ’€ ์ˆ˜ ์žˆ๋Š” ์ง„์งœ mutex๊ณ  Lock์€ ์†Œ์œ ๊ถŒ์ด ์—†์–ด ์—„๋ฐ€ํžˆ๋Š” binary semaphore์— ๊ฐ€๊น๋‹ค. ์‹ค๋ฌด์—์„  ๋‘˜ ๋‹ค ๊ทธ๋ƒฅ โ€œlockโ€์ด๋ผ ๋ถ€๋ฅธ๋‹ค.

Event๋กœ ์ถœ๋ฐœ ์‹ ํ˜ธ ๋งž์ถ”๊ธฐ: ์—ฌ๋Ÿฌ worker๋ฅผ ํ•œ ์‹ ํ˜ธ์— ํ•จ๊ป˜ ์ถœ๋ฐœ์‹œํ‚จ๋‹ค. set()์€ ํ”Œ๋ž˜๊ทธ๋ฅผ ์˜ฌ๋ฆฌ๊ณ  wait()์€ ์˜ฌ๋ผ๊ฐˆ ๋•Œ๊นŒ์ง€ Blocked๋กœ ์ž”๋‹ค.

start = threading.Event()
 
def worker():
    start.wait()          # set๋  ๋•Œ๊นŒ์ง€ Blocked๋กœ ๋Œ€๊ธฐ
    print("์ถœ๋ฐœ")
 
threading.Thread(target=worker).start()
start.set()               # ๋Œ€๊ธฐํ•˜๋˜ worker๋ฅผ ๊นจ์›€

Semaphore๋กœ ๋™์‹œ ์‹คํ–‰ ์ˆ˜ ์ œํ•œ: โ€œ๋™์‹œ K๊ฐœ๊นŒ์ง€๋งŒโ€์„ ๊ฐ•์ œํ•œ๋‹ค.

sem = threading.Semaphore(3)   # ๋™์‹œ 3๊ฐœ๊นŒ์ง€ ํ—ˆ์šฉ
 
def fetch():
    with sem:                  # ์•ž์„  3๊ฐœ๊ฐ€ ๋‹ค ์ฐจ ์žˆ์œผ๋ฉด 4๋ฒˆ์งธ๋Š” ๋Œ€๊ธฐ
        ...  # ์™ธ๋ถ€ API ํ˜ธ์ถœ ๋“ฑ

thread ๊ฐ„ ํ†ต์‹  โ€” queue.Queue

๊ณต์œ  ๋ณ€์ˆ˜๋ฅผ lock์œผ๋กœ ์ง€ํ‚ค๋Š” ๋Œ€์‹ , thread-safeํ•œ queue.Queue๋กœ ๊ฐ’์„ ์ฃผ๊ณ ๋ฐ›๋Š” ํŽธ์ด ์•ˆ์ „ํ•˜๋‹ค. producer๊ฐ€ put(), consumer๊ฐ€ get()ํ•˜๋ฉฐ ๋‚ด๋ถ€์ ์œผ๋กœ lock์„ ์•Œ์•„์„œ ์ฒ˜๋ฆฌํ•œ๋‹ค. ๊ฐ’์„ ์ฃผ๊ณ ๋ฐ›์•„ ๊ณต์œ ํ•˜๋Š” ์ด ๋ฐœ์ƒ์€ Go์˜ channel๊ณผ ๊ฐ™๋‹ค(๋ฌด๋ฒ„ํผ ๋ž‘๋ฐ๋ถ€ยทselectยทclose ๊ฐ™์€ ์ฑ„๋„ ๊ณ ์œ  ๊ธฐ๋Šฅ์€ ์—†๋‹ค).

import queue, threading
 
q = queue.Queue()
 
def producer():
    for i in range(5):
        q.put(i)
 
def consumer():
    while True:
        item = q.get()      # ํ๊ฐ€ ๋น„๋ฉด ๋Œ€๊ธฐ(blocking)
        print(item)
        q.task_done()

threading์€ thread์˜ ์ƒ๋ช…์ฃผ๊ธฐ์™€ ๋™๊ธฐํ™”๋ฅผ ์„ธ๋ฐ€ํ•˜๊ฒŒ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ๊ทธ๋งŒํผ ์ฝ”๋“œ๊ฐ€ ๋Š˜๊ณ  deadlockยทrace condition์„ ์ง์ ‘ ์‹ ๊ฒฝ ์จ์•ผ ํ•œ๋‹ค. ๋‹จ์ˆœํžˆ โ€œํ•จ์ˆ˜ N๊ฐœ๋ฅผ ๋™์‹œ์— ๋Œ๋ฆฌ๊ณ  ๊ฒฐ๊ณผ๋ฅผ ๋ชจ์œผ๋Š”โ€ ์šฉ๋„๋ผ๋ฉด 4์ ˆ์˜ concurrent.futures๊ฐ€ ๋” ๊ฐ„๊ฒฐํ•˜๋‹ค.


3. multiprocessing โ€” process ์ €์ˆ˜์ค€

CPU-bound ์ž‘์—…์„ ์—ฌ๋Ÿฌ ์ฝ”์–ด์— ์‹ค์ œ๋กœ ๋‚˜๋ˆ„๋ ค๋ฉด process๋ฅผ ์“ด๋‹ค. ๊ฐ process๋Š” ์ž๊ธฐ๋งŒ์˜ Python ์ธํ„ฐํ”„๋ฆฌํ„ฐ์™€ GIL์ด ์žˆ์œผ๋ฏ€๋กœ GIL ์ œ์•ฝ์„ ์šฐํšŒํ•œ๋‹ค.

process ์ƒ์„ฑ๊ณผ ์‹คํ–‰

from multiprocessing import Process
 
def worker(name):
    print(f"{name} ๊ณ„์‚ฐ ์ค‘")
 
if __name__ == "__main__":       # spawn ๋ฐฉ์‹์—์„œ ํ•„์ˆ˜
    p = Process(target=worker, args=("A",))
    p.start()
    p.join()

์ธํ„ฐํŽ˜์ด์Šค๋Š” threading.Thread์™€ ๋‹ฎ์•˜์ง€๋งŒ(start/join), ์‹คํ–‰ ๋‹จ์œ„๊ฐ€ ๋…๋ฆฝ process๋ผ๋Š” ์ ์ด ๋‹ค๋ฅด๋‹ค.

Pool โ€” ์›Œ์ปค process ํ’€

process๋ฅผ ๋งค๋ฒˆ ๋งŒ๋“ค์ง€ ์•Š๊ณ  ๋ฏธ๋ฆฌ ๋ช‡ ๊ฐœ ๋„์›Œ๋‘๊ณ  ์ž‘์—…์„ ๋‚˜๋ˆ  ์ค€๋‹ค.

from multiprocessing import Pool
 
def square(x):
    return x * x
 
if __name__ == "__main__":
    with Pool(4) as pool:
        results = pool.map(square, range(10))   # 4๊ฐœ process์— ๋ถ„์‚ฐ

IPC โ€” process ๊ฐ„ ํ†ต์‹ 

process๋Š” ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ๊ฒฉ๋ฆฌ๋ผ ์žˆ์–ด ๊ณต์œ  ๋ณ€์ˆ˜๋กœ ๊ฐ’์„ ๋ชป ๋„˜๊ธด๋‹ค. ํ†ต์‹  ์ˆ˜๋‹จ์ด ๋”ฐ๋กœ ํ•„์š”ํ•˜๋‹ค.

  • Queue / Pipe: process ์‚ฌ์ด๋กœ ๊ฐ’์„ ์ฃผ๊ณ ๋ฐ›๋Š” ํ†ต๋กœ. ๋„˜๊ธฐ๋Š” ๊ฐ’์€ pickle๋กœ ์ง๋ ฌํ™”๋œ๋‹ค.
  • shared memory(Value, Array): ์—ฌ๋Ÿฌ process๊ฐ€ ๊ณต์œ ํ•˜๋Š” ๋ฉ”๋ชจ๋ฆฌ ๋ธ”๋ก. ์ง๋ ฌํ™” ์—†์ด ๊ฐ™์€ ๊ฐ’์„ ๋ณธ๋‹ค.

๋„˜๊ธฐ๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ํฌ๋ฉด ์ด ์ง๋ ฌํ™”ยท์ „์†ก ๋น„์šฉ์ด ๊ณ„์‚ฐ ์ด๋“์„ ๊นŽ์•„๋จน์„ ์ˆ˜ ์žˆ๋‹ค. ํฐ ๋ฐ์ดํ„ฐ๋ฅผ ์ž์ฃผ ์˜ค๊ฐ€๋Š” ๊ตฌ์กฐ๋ผ๋ฉด ์˜คํžˆ๋ ค ๋А๋ ค์ง„๋‹ค.

fork์™€ spawn

์ž์‹ process๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐฉ์‹์ด ๋‘˜์ด๋‹ค.

  • fork(๋ฆฌ๋ˆ…์Šค ๊ธฐ๋ณธ): ๋ถ€๋ชจ process์˜ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ๋ณต์‚ฌํ•ด ์ž์‹์„ ๋งŒ๋“ ๋‹ค. ๋ถ€๋ชจ ์ƒํƒœ๋ฅผ ๊ทธ๋Œ€๋กœ ๋ฌผ๋ ค๋ฐ›๋Š”๋‹ค.
  • spawn(์œˆ๋„์šฐยทmacOS ๊ธฐ๋ณธ): ์ƒˆ Python ์ธํ„ฐํ”„๋ฆฌํ„ฐ๋ฅผ ์ฒ˜์Œ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๊ณ  ํ•„์š”ํ•œ ๊ฒƒ๋งŒ pickle๋กœ ์ „๋‹ฌํ•œ๋‹ค. ๊ทธ๋ž˜์„œ ์ž์‹์ด ๋ถ€๋ชจ ์ƒํƒœ๋ฅผ ์ž๋™์œผ๋กœ ๋ฌผ๋ ค๋ฐ›์ง€ ์•Š๊ณ  ์‹คํ–‰ ์ฝ”๋“œ๋Š” if __name__ == "__main__": ์•„๋ž˜์— ๋‘ฌ์•ผ ๋ฌดํ•œ ์žฌ์‹คํ–‰์„ ๋ง‰๋Š”๋‹ค.
์™œ spawn์€ __name__ ๊ฐ€๋“œ๊ฐ€ ํ•„์š”ํ•œ๊ฐ€

๊ฐ€๋“œ๊ฐ€ ํ•„์š”ํ•œ ์ด์œ ๋Š” spawn ์ž์‹์ด ๋ถ€๋ชจ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋‹ค์‹œ importํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๋นˆ ์ธํ„ฐํ”„๋ฆฌํ„ฐ๋กœ ๋œฌ ์ž์‹์€ ์‹คํ–‰ํ•  ํ•จ์ˆ˜ ์ •์˜๋ฅผ ์–ป์œผ๋ ค main ๋ชจ๋“ˆ์„ ์œ„โ†’์•„๋ž˜๋กœ ๋‹ค์‹œ ์‹คํ–‰ํ•˜๋Š”๋ฐ, ์ด๋•Œ process๋ฅผ ๋งŒ๋“œ๋Š” ์ฝ”๋“œ๊ฐ€ ์ตœ์ƒ๋‹จ์— ๋…ธ์ถœ๋ผ ์žˆ์œผ๋ฉด ๊ทธ ์ฝ”๋“œ๋„ ์žฌ์‹คํ–‰๋ผ ์ž์‹์ด ๋˜ ์ž์‹์„ ๋‚ณ๋Š” ์žฌ๊ท€ ์ƒ์„ฑ์ด ๋œ๋‹ค(ํ˜„๋Œ€ CPython์€ ์ด๋ฅผ ๊ฐ์ง€ํ•ด RuntimeError๋กœ ๋ง‰๋Š”๋‹ค). __name__์€ ์ง์ ‘ ์‹คํ–‰ํ•œ ๋ถ€๋ชจ์—์„  "__main__", import๋œ ์ž์‹์—์„  ๋ชจ๋“ˆ ์ด๋ฆ„์ด๋ผ, ๊ฐ€๋“œ ์•ˆ์˜ ์ฝ”๋“œ๋Š” ๋ถ€๋ชจ์—์„œ๋งŒ ๋Œ๊ณ  ์ž์‹์€ ๊ฑด๋„ˆ๋›ด๋‹ค.

๊ทธ๋ž˜์„œ ๋ฐฐ์น˜ ์›์น™์ด ๊ฐˆ๋ฆฐ๋‹ค โ€” ํ•จ์ˆ˜ยทํด๋ž˜์Šค ์ •์˜๋Š” ๊ฐ€๋“œ ๋ฐ–์—(์ž์‹์ด importํ•ด ๊ฐ€์ ธ๊ฐ€์•ผ ํ•˜๋ฏ€๋กœ), process๋ฅผ ๋งŒ๋“ค๊ณ  ์‹œ์ž‘ํ•˜๋Š” ์ฝ”๋“œ๋Š” ๊ฐ€๋“œ ์•ˆ์— ๋‘”๋‹ค. ์ฐธ๊ณ ๋กœ ์ด ๊ฐ€๋“œ๊ฐ€ ํ•„์š”ํ•œ ๊ฑด Python์ด ๋ชจ๋“ˆ์„ importํ•  ๋•Œ top-level ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๊ณ  ๋ช…์‹œ์  main() ์ง„์ž…์ ์„ ์“ฐ๋Š” ์–ธ์–ด(CยทGoยทJava ๋“ฑ)์—๋Š” ์ด footgun์ด ์—†๋‹ค. fork๋Š” ์žฌimport๋ฅผ ์•ˆ ํ•˜๋‹ˆ ๊ฐ€๋“œ๊ฐ€ ์—†์–ด๋„ ๋˜์ง€๋งŒ ์ด์‹์„ฑ์„ ์œ„ํ•ด ๋ถ™์—ฌ๋‘๋Š” ๊ฒŒ ๊ด€๋ก€๋‹ค.


4. concurrent.futures โ€” ๊ณ ์ˆ˜์ค€ ํ†ต์ผ ์ธํ„ฐํŽ˜์ด์Šค

threadingยทmultiprocessing์„ ์ง์ ‘ ๋‹ค๋ฃจ๋Š” ๋Œ€์‹ , ์ž‘์—…์„ ํ’€์— ๋˜์ง€๊ณ  ๊ฒฐ๊ณผ๋ฅผ Future๋กœ ๋ฐ›๋Š” ํ†ต์ผ๋œ ๊ณ ์ˆ˜์ค€ ์ธํ„ฐํŽ˜์ด์Šค๋‹ค. ์˜ˆ์™ธยทํƒ€์ž„์•„์›ƒยท์ทจ์†Œยท๊ฒฐ๊ณผ ์ˆ˜์ง‘์ด Future๋กœ ์ •๋ฆฌ๋ผ ์ฝ”๋“œ๊ฐ€ ์งง๋‹ค.

ํ•ต์‹ฌ 3์š”์†Œ โ€” Executor, Future, submit

  • Executor: ์ž‘์—…์„ ๋งก๋Š” ํ’€. ThreadPoolExecutor(thread ํ’€)์™€ ProcessPoolExecutor(process ํ’€) ๋‘˜.
  • submit(func, *args): ์ž‘์—…์„ ํ’€์— ๋˜์ง€๊ณ  ์ฆ‰์‹œ Future๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
  • Future: โ€œ๋ฏธ๋ž˜์— ์ฑ„์›Œ์งˆ ๊ฒฐ๊ณผโ€๋ฅผ ๋‹ด๋Š” ๊ฐ์ฒด. ์ง€๊ธˆ์€ ๋น„์–ด ์žˆ๊ณ  ์ž‘์—…์ด ๋๋‚˜๋ฉด ๊ฒฐ๊ณผ๋‚˜ ์˜ˆ์™ธ๊ฐ€ ๋‹ด๊ธด๋‹ค. ์‹คํ–‰ ํ๋ฆ„์ด ์•„๋‹ˆ๋ผ ๊ฒฐ๊ณผ๋ฅผ ๋‹ด๋Š” ๊ทธ๋ฆ‡ ์ด๋‹ค.
from concurrent.futures import ThreadPoolExecutor
 
def fetch(url):
    ...  # I/O ์ž‘์—…
    return len(url)
 
with ThreadPoolExecutor(max_workers=8) as executor:
    future = executor.submit(fetch, "https://example.com")
    result = future.result()   # ์™„๋ฃŒ๊นŒ์ง€ ๋Œ€๊ธฐ ํ›„ ๊ฒฐ๊ณผ ํšŒ์ˆ˜
  • I/O-bound โ†’ ThreadPoolExecutor, CPU-bound โ†’ ProcessPoolExecutor. ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ๊ฐ™์•„ ํ•œ ์ค„๋งŒ ๋ฐ”๊พธ๋ฉด ๋œ๋‹ค.

์—ฌ๋Ÿฌ ์ž‘์—… ์กฐ์œจ

from concurrent.futures import ThreadPoolExecutor, as_completed
 
with ThreadPoolExecutor(max_workers=8) as executor:
    futures = [executor.submit(fetch, u) for u in urls]
    for f in as_completed(futures):   # ์™„๋ฃŒ๋œ ๊ฒƒ๋ถ€ํ„ฐ
        print(f.result())

์ž‘์—…์„ ์—ฌ๋Ÿฌ ๊ฐœ submitํ•˜๋ฉด max_workers ๊ฐœ์ˆ˜๋งŒํผ๋งŒ ๋™์‹œ์— ๋Œ๊ณ  ๋‚˜๋จธ์ง€๋Š” ๋‚ด๋ถ€ FIFO ํ์—์„œ ๋Œ€๊ธฐํ•˜๋‹ค ์›Œ์ปค๊ฐ€ ๋น„๋Š” ๋Œ€๋กœ submit ์ˆœ์„œ๋Œ€๋กœ ํˆฌ์ž…๋œ๋‹ค(์˜ˆ: max_workers=8์— 100๊ฐœ๋ฅผ ๋˜์ง€๋ฉด 8๊ฐœ๊ฐ€ ๋Œ๊ณ  92๊ฐœ๋Š” ํ์—์„œ ์ˆœ์„œ๋Œ€๋กœ ๋Œ€๊ธฐ). ์‹œ์ž‘ ์‹œ์ ์„ ๋งž์ถ”๋ ค Event ๊ฐ™์€ ๊ฑธ ๊ฑธ ํ•„์š” ์—†์ด executor๊ฐ€ ์•Œ์•„์„œ ๋ฐฐ๋ถ„ํ•œ๋‹ค. ๋‹จ ์™„๋ฃŒ ์ˆœ์„œ๋Š” ๋ณด์žฅ๋˜์ง€ ์•Š์•„ ๋จผ์ € ๋๋‚œ ๊ฒƒ๋ถ€ํ„ฐ ๋ฐ›์œผ๋ ค๋ฉด ์•„๋ž˜ as_completed๋ฅผ ์“ด๋‹ค. submit์€ ์‹œ์ž‘์„ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๊ณ  ์ฆ‰์‹œ Future๋ฅผ ๋Œ๋ ค์ฃผ๋Š” non-blocking ํ˜ธ์ถœ์ด๋‹ค.

  • executor.map(func, iterable): ๊ฒฐ๊ณผ๋ฅผ ์ž…๋ ฅ ์ˆœ์„œ๋Œ€๋กœ ๋Œ๋ ค์ค€๋‹ค.
  • as_completed(futures): ์™„๋ฃŒ๋œ ์ˆœ์„œ๋Œ€๋กœ ๊บผ๋‚ธ๋‹ค. ๋จผ์ € ๋๋‚œ ๊ฒƒ๋ถ€ํ„ฐ ์ฒ˜๋ฆฌํ•  ๋•Œ.
  • wait(futures, ...): ์™„๋ฃŒ ์กฐ๊ฑด(์ „๋ถ€/ํ•˜๋‚˜/ํƒ€์ž„์•„์›ƒ)์„ ์ง€์ •ํ•ด ๋Œ€๊ธฐ.

Future ๋‹ค๋ฃจ๊ธฐ

  • result(timeout=None): ์™„๋ฃŒ๊นŒ์ง€ ๋Œ€๊ธฐ ํ›„ ๊ฒฐ๊ณผ. ํƒ€์ž„์•„์›ƒ ์ง€์ • ๊ฐ€๋Šฅ. ์ž‘์—…์ด ์˜ˆ์™ธ๋กœ ๋๋‚ฌ์œผ๋ฉด ๊ทธ ์˜ˆ์™ธ๊ฐ€ ์—ฌ๊ธฐ์„œ ๋‹ค์‹œ ํ„ฐ์ง„๋‹ค.
  • done(): ์™„๋ฃŒ ์—ฌ๋ถ€๋งŒ non-blocking์œผ๋กœ ํ™•์ธ.
  • exception(): ์˜ˆ์™ธ ๊ฐ์ฒด๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค(์žˆ์œผ๋ฉด).
  • cancel(): ์•„์ง ์‹œ์ž‘ ์ „์ด๋ฉด ์ทจ์†Œ๋ฅผ ์‹œ๋„ํ•œ๋‹ค.

5. ๋ฌด์—‡์„ ์–ธ์ œ ์“ฐ๋‚˜

์ƒํ™ฉ๋„๊ตฌ
I/O-bound, ์ ์€ยท์ค‘๊ฐ„ ๊ทœ๋ชจ ๋™์‹œ ์ž‘์—…ThreadPoolExecutor / threading
CPU-boundProcessPoolExecutor / multiprocessing
I/O-bound, ๋Œ€๋Ÿ‰(์ˆ˜์ฒœ-์ˆ˜๋งŒ)asyncio

์„ ํƒ์˜ ํฐ ๊ฐˆ๋ž˜๋Š” ์ด๋ ‡๋‹ค.

  • ์ž‘์—…์ด ๋А๋ฆฐ ์›์ธ๋ถ€ํ„ฐ ๊ตฌ๋ถ„ํ•œ๋‹ค. CPU๊ฐ€ ๋ฐ”์˜๋ฉด(CPU-bound) process, ๋Œ€๊ธฐ๊ฐ€ ๊ธธ๋ฉด(I/O-bound) thread๋‚˜ asyncio๋‹ค. GIL ๋•Œ๋ฌธ์— CPU-bound๋ฅผ thread๋กœ ๋‚˜๋ˆ„๋Š” ๊ฑด ํ—›์ˆ˜๊ณ ๋‹ค.
  • ์ €์ˆ˜์ค€ ์ง์ ‘ vs ๊ณ ์ˆ˜์ค€. threadยทprocess์˜ ์ƒ๋ช…์ฃผ๊ธฐ๋‚˜ ๋™๊ธฐํ™”๋ฅผ ์„ธ๋ฐ€ํ•˜๊ฒŒ ์ œ์–ดํ•ด์•ผ ํ•˜๋ฉด threadingยทmultiprocessing์„ ์ง์ ‘ ์“ฐ๊ณ  โ€œํ•จ์ˆ˜ N๊ฐœ๋ฅผ ๋™์‹œ์— ๋Œ๋ ค ๊ฒฐ๊ณผ๋ฅผ ๋ชจ์œผ๋Š”โ€ ํ”ํ•œ ํŒจํ„ด์ด๋ฉด concurrent.futures๊ฐ€ ์งง๊ณ  ์•ˆ์ „ํ•˜๋‹ค.
  • ๋™์‹œ ์ž‘์—…์ด ์ˆ˜์ฒœ ๊ฐœ ์ด์ƒ์ธ I/O๋ผ๋ฉด thread ์ˆ˜์ฒœ ๊ฐœ๋Š” ์ „ํ™˜ ๋น„์šฉ์ด ์ปค์ง„๋‹ค. ์ด๋•Œ๋Š” ๋‹จ์ผ thread์—์„œ coroutine์„ ๊ตด๋ฆฌ๋Š” asyncio๊ฐ€ ๋‚ซ๋‹ค.
  • ์„ž์—ฌ ์žˆ์œผ๋ฉด ๋‚˜๋ˆ  ๋งก๊ธด๋‹ค. ๋„คํŠธ์›Œํฌ I/O๋Š” asyncio๋กœ ๊ฒน์น˜๊ณ  CPU ํ›„์ฒ˜๋ฆฌ๋Š” process pool๋กœ ๋ถ„๋ฆฌํ•˜๊ณ  ๋™๊ธฐ ์ „์šฉ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” thread๋กœ ์šฐํšŒํ•˜๋Š”(asyncio.to_thread) ์‹์œผ๋กœ ์‹ค๋ฌด์—์„œ ํ”ํžˆ ์กฐํ•ฉํ•œ๋‹ค.

๊ณต์œ  ์ž์›์„ ์—ฌ๋Ÿฟ์ด ๊ฑด๋“œ๋ฆฌ๋ฉด ์–ด๋А ๋ชจ๋ธ์ด๋“  race condition์„ ์‹ ๊ฒฝ ์จ์•ผ ํ•œ๋‹ค. thread๋Š” lockยทqueue๋กœ, process๋Š” ๊ฒฉ๋ฆฌ ๋•์— ๊ณต์œ  ๋ฉ”๋ชจ๋ฆฌ ๊ฒฝ์Ÿ์€ ์ ์ง€๋งŒ ๊ณต์œ  ํŒŒ์ผยทDB ๊ฒฝ์Ÿ์€ ๋‚จ๋Š”๋‹ค.