ML/MLOps Engineer
Python: Parallelism(๋ณ‘๋ ฌ์ฒ˜๋ฆฌ) Vs Concurrency(๋™์‹œ์„ฑ) Vs Threading(์Šค๋ ˆ๋”ฉ)

Python: Parallelism Vs Concurrency Vs Threading (opens new window)์„ ์ฝ๊ณ  ์ •๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์—ฌ๋Ÿฌ ์ž‘์—…์„ ๊ด€๋ฆฌํ•˜๊ณ , ํ”„๋กœ์„ธ์Šค๋ฅผ ๊ฐ€์†ํ™”ํ•˜๋ฉฐ, ๋…๋ฆฝ์ ์ธ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ๋ณด์žฅํ•˜๋ ค๋ฉด ๋™์‹œ์„ฑ(Concurrency), ์Šค๋ ˆ๋”ฉ(Threading), ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ(Parallelism)๋ฅผ ์ดํ•ดํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ์ฒ˜์Œ์—๋Š” ์ด ๊ฐœ๋…๋“ค์ด ๋น„์Šทํ•˜๊ฒŒ ๋А๊ปด์งˆ ์ˆ˜ ์žˆ์ง€๋งŒ ์„œ๋กœ ์ฐจ์ด๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ๋งˆ์น˜ ์นดํŽ˜๊ฐ€ ์ง์›๊ณผ ์žฅ๋น„๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์„ ์˜ˆ์‹œ๋กœ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.

์ด ๊ธ€์—์„œ๋Š” ๊ธฐ์ˆ ์ ์ธ ๊ฐœ๋…์„ ์ ‘๊ทผํ•˜๊ธฐ ์‰ฝ๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด ์นดํŽ˜ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ํ†ตํ•ด ์ด๋Ÿฌํ•œ ํŒจ๋Ÿฌ๋‹ค์ž„์„ ํƒ๊ตฌํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋˜ํ•œ Python์˜ ๊ฐ•๋ ฅํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ธ asyncio, threading, multiprocessing์„ ํ™œ์šฉํ•œ ์˜ˆ์‹œ๋„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. ๋‹จ์ผ ์Šค๋ ˆ๋“œ๋ฅผ ๊ด€๋ฆฌํ•˜๋“ , ๋‹ค์ค‘ ์ฝ”์–ด๋ฅผ ํ™œ์šฉํ•˜๋“ , ์ด ๊ฐœ๋…์„ ํšจ๊ณผ์ ์œผ๋กœ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋” ๋ช…ํ™•ํžˆ ์ดํ•ดํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

Concurrency(๋™์‹œ์„ฑ)๐Ÿ•’

ํ˜ผ์ž ์šด์˜ํ•˜๋Š” ์นดํŽ˜๋ฅผ ์ƒ์ƒํ•ด๋ณด์„ธ์š”. ํ•œ ๊ณ ๊ฐ์˜ ์ปคํ”ผ๋ฅผ ์ถ”์ถœํ•˜๋Š” ๋™์•ˆ, ๋‹ค๋ฅธ ๊ณ ๊ฐ์˜ ์ฃผ๋ฌธ์„ ๋ฐ›๊ณ , ์šฐ์œ ๋ฅผ ์ŠคํŒ€ํ•˜๊ณ , ๋˜ ๋‹ค๋ฅธ ๊ณ ๊ฐ์˜ ์ƒŒ๋“œ์œ„์น˜๋ฅผ ์ค€๋น„ํ•ฉ๋‹ˆ๋‹ค.

ํ•œ ๋ฒˆ์— ๋ชจ๋“  ์ž‘์—…์„ ํ•˜์ง€ ์•Š์ง€๋งŒ, ์ž‘์—… ๊ฐ„ ์ „ํ™˜์„ ํ†ตํ•ด ๋ชจ๋“  ์ฃผ๋ฌธ์„ ํšจ์œจ์ ์œผ๋กœ ์ง„ํ–‰์‹œํ‚ต๋‹ˆ๋‹ค.

โžก๏ธ Python์—์„œ ๊ตฌํ˜„ ๋ฐฉ๋ฒ•:

import asyncio

async def task(name, delay):
    print(f"{name} started")
    await asyncio.sleep(delay)
    print(f"{name} completed")

async def main():
    task1 = asyncio.create_task(task("Task1", 2))
    task2 = asyncio.create_task(task("Task2", 3))

    print("Tasks are running concurrently...")

    await task1
    await task2

    print("Both tasks are completed")

asyncio.run(main())

๋™์‹œ์„ฑ์€ asyncio ๊ฐ™์€ ํŒจํ‚ค์ง€๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ชจ๋“  ์ž‘์—…์„ ๋™์‹œ์— ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š๋”๋ผ๋„ ์ž‘์—… ๊ฐ„ ์ „ํ™˜์„ ํ†ตํ•ด ์—ฌ๋Ÿฌ ์ž‘์—…์„ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

Threading(์Šค๋ ˆ๋”ฉ) ๐Ÿงต

๋ถ„์ฃผํ•œ ์ปคํ”ผ์ˆ์„ ์šด์˜ํ•œ๋‹ค๊ณ  ์ƒ์ƒํ•ด๋ณด์„ธ์š”. ์—ฌ๋Ÿฌ ๋ช…์˜ ์ž์‹ ์ด ๋ณต์ œ๋˜์–ด ๊ฐ๊ธฐ ๋‹ค๋ฅธ ์ž‘์—…์— ์ง‘์ค‘ํ•ฉ๋‹ˆ๋‹ค. ํ•œ ์‚ฌ๋žŒ์€ ์ปคํ”ผ๋ฅผ ์ถ”์ถœํ•˜๊ณ , ๋‹ค๋ฅธ ์‚ฌ๋žŒ์€ ํŽ˜์ด์ŠคํŠธ๋ฆฌ๋ฅผ ์ค€๋น„ํ•˜๋ฉฐ, ๋˜ ๋‹ค๋ฅธ ์‚ฌ๋žŒ์€ ์Šค๋ฌด๋””๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

์ด ๋ณต์ œ๋œ ์ž์‹ (์Šค๋ ˆ๋“œ)์€ ๊ฐ์ž์˜ ์ž‘์—…์„ ๋…๋ฆฝ์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•˜์ง€๋งŒ, ๋ชจ๋‘ ๊ฐ™์€ ๊ณต๊ฐ„๊ณผ ์žฅ๋น„๋ฅผ ๊ณต์œ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ„ํ˜น ๋Œ€๊ธฐ ์ƒํƒœ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

โžก๏ธ Python์—์„œ ๊ตฌํ˜„ ๋ฐฉ๋ฒ•:

import threading
import time

def task(name, delay):
    print(f"{name} started")
    time.sleep(delay)
    print(f"{name} completed")

if __name__ == '__main__':
    thread1 = threading.Thread(target=task, args=('Thread1', 2))
    thread2 = threading.Thread(target=task, args=('Thread2', 3))

    print("Threads are running concurrently...")

    thread1.start()
    thread2.start()

    thread1.join()
    thread2.join()

    print("Both Threads completed")

์Šค๋ ˆ๋”ฉ์€ ํ”„๋กœ๊ทธ๋žจ์˜ ๋‹ค๋ฅธ ๋ถ€๋ถ„์„ ๋ณ„๋„ ์Šค๋ ˆ๋“œ๋กœ ์‹คํ–‰์‹œ์ผœ ๋™์ผํ•œ ๋ฉ”๋ชจ๋ฆฌ์™€ ์ž์›์„ ๊ณต์œ ํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. ์ž‘์—…์€ ๋™์‹œ์— ์‹คํ–‰๋˜์ง€๋งŒ, ์‹ค์ œ๋กœ๋Š” CPU๊ฐ€ ์Šค๋ ˆ๋“œ ๊ฐ„์„ ์ „ํ™˜ํ•˜์—ฌ ์‹คํ–‰ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์™„์ „ํžˆ ๋™์‹œ์— ์‹คํ–‰๋˜์ง€๋Š” ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Parallelism(๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ) ๐ŸŽ๏ธ

๊ฐ์ž ์™„๋ฒฝํžˆ ์žฅ๋น„๋ฅผ ๊ฐ–์ถ˜ ์—ฌ๋Ÿฌ ๋ฐ”๋ฆฌ์Šคํƒ€๊ฐ€ ์žˆ๋Š” ๋Œ€ํ˜• ์นดํŽ˜๋ฅผ ์ƒ์ƒํ•ด๋ณด์„ธ์š”. ํ•œ ๋ฐ”๋ฆฌ์Šคํƒ€๋Š” ์ปคํ”ผ๋ฅผ ๋งŒ๋“ค๊ณ , ๋˜ ๋‹ค๋ฅธ ๋ฐ”๋ฆฌ์Šคํƒ€๋Š” ์Šค๋ฌด๋””๋ฅผ ๋ธ”๋ Œ๋”ฉํ•˜๋ฉฐ, ์„ธ ๋ฒˆ์งธ ๋ฐ”๋ฆฌ์Šคํƒ€๋Š” ์ƒŒ๋“œ์œ„์น˜๋ฅผ ์ค€๋น„ํ•ฉ๋‹ˆ๋‹ค.

๊ฐ ๋ฐ”๋ฆฌ์Šคํƒ€๋Š” ์ž์‹ ์˜ ์ž์›์„ ๊ฐ€์ง€๊ณ  ๋…๋ฆฝ์ ์œผ๋กœ ์ž‘์—…ํ•˜๋ฏ€๋กœ ์„œ๋กœ ๊ธฐ๋‹ค๋ฆฌ๊ฑฐ๋‚˜ ๊ฐ„์„ญํ•˜์ง€ ์•Š๊ณ  ๋™์‹œ์— ๊ณ ๊ฐ์„ ์‘๋Œ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

โžก๏ธ Python์—์„œ ๊ตฌํ˜„ ๋ฐฉ๋ฒ•:

import multiprocessing
import time

def task(name, delay):
    print(f"{name} started")
    time.sleep(delay)
    print(f"{name} completed")

if __name__ == '__main__':
    process1 = multiprocessing.Process(target=task, args=("Process 1", 2))
    process2 = multiprocessing.Process(target=task, args=("Process 2", 3))

    print("Processes are running in parallel...")

    process1.start()
    process2.start()

    process1.join()
    process2.join()

    print("Both processes completed")

๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๋Š” ์—ฌ๋Ÿฌ CPU ์ฝ”์–ด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‹ค์ œ๋กœ ์—ฌ๋Ÿฌ ์ž‘์—…์„ ๋™์‹œ์— ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. multiprocessing ๊ฐ™์€ ํŒจํ‚ค์ง€๋Š” ๋ณ„๋„ ํ”„๋กœ์„ธ์Šค๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ํ”„๋กœ์„ธ์„œ๋ฅผ ๋ถ„๋ฆฌ๋œ ์ฝ”์–ด์—์„œ ๋…๋ฆฝ์ ์œผ๋กœ ์‹คํ–‰ํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿง  ๋™์‹œ์„ฑ๊ณผ ์Šค๋ ˆ๋”ฉ์˜ ์ฐจ์ด์ :
โžก๏ธ ๋™์‹œ์„ฑ: ์—ฌ๋Ÿฌ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์š”๋ฆฌ์‚ฌ๊ฐ€ ๊ฐ ์ž‘์—… ๊ฐ„ ์ „ํ™˜ํ•˜๋ฉฐ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
โžก๏ธ ์Šค๋ ˆ๋”ฉ: ์—ฌ๋Ÿฌ ์š”๋ฆฌ์‚ฌ(์Šค๋ ˆ๋“œ)๊ฐ€ ๋™์‹œ์— ์ž‘์—…ํ•˜์ง€๋งŒ ๋™์ผํ•œ ์ฃผ๋ฐฉ ์ž์›์„ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค.

๋™์‹œ์„ฑ์€ ์ž‘์—…์„ ๊ด€๋ฆฌํ•˜๊ณ  ์ „ํ™˜ํ•˜๋Š” ๋ฐฉ์‹์— ์ดˆ์ ์„ ๋งž์ถ”๋Š” ๋ฐ˜๋ฉด, ์Šค๋ ˆ๋”ฉ์€ ์ž‘์—…์„ ์—ฌ๋Ÿฌ ์Šค๋ ˆ๋“œ๋กœ ๋‚˜๋ˆ  ๋™์‹œ์„ฑ์„ ๊ตฌํ˜„ํ•˜๋Š” ํ•œ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

๊ฒฐ๋ก 

๋™์‹œ์„ฑ, ์Šค๋ ˆ๋”ฉ, ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ ์ค‘ ๋ฌด์—‡์„ ์„ ํƒํ• ์ง€๋Š” ์ž‘์—…์˜ ํŠน์„ฑ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง‘๋‹ˆ๋‹ค.

  • ๋™์‹œ์„ฑ (asyncio): CPU ๋ถ€ํ•˜๊ฐ€ ์ ์€ I/O ์ค‘์‹ฌ ์ž‘์—…์— ์ ํ•ฉ.
  • ์Šค๋ ˆ๋”ฉ: ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ๊ณต์œ ํ•˜๋ฉฐ, CPU ์ง‘์•ฝ์ ์ด์ง€ ์•Š์€ ์ž‘์—…์— ์ ํ•ฉ.
  • ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ (multiprocessing): CPU ์ง‘์•ฝ์  ์ž‘์—…์— ์ ํ•ฉํ•˜๋ฉฐ, ์‹ค์ œ๋กœ ๋™์‹œ ์‹คํ–‰์ด ํ•„์š”ํ•  ๋•Œ ์‚ฌ์šฉ.

์ด ์ฐจ์ด๋ฅผ ์ดํ•ดํ•˜๋ฉด ์ž์›์„ ํšจ์œจ์ ์œผ๋กœ ํ™œ์šฉํ•˜๊ณ  ํ”„๋กœ๊ทธ๋žจ ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.