카테고리 없음

[CS] 멀티스레딩과 멀티프로세싱

sian han 2025. 8. 7. 16:16

▶ 프로세스

컴퓨에서 실행중인 프로그램

각각의 프로세스는 독립된 메모리 공간을 할당 받는다

 

 메인 메모리

프로세스가 CPU 에서 실행되기 위해 대기하는 곳

 

IO(input/output)

파일을 읽고 쓰거나, 네트워크의 어딘가와 데이터를 주고 받는 것

입출력 장치(마우스, 키보드 등)와 데이터를 주거나 받는 것

 

 단일 프로세스

  • 한 번에 하나의 프로그램만 실행됨
  • 단점 : CPU 사용률이 좋지 않음
  • 해결책 : "여러개의 프로그램을 메모리에 올려놓고 동시에 실행시키자" 라는 아이디어. IO 작업이 발생하면 다른 프로세스가 CPU 에서 실행됨. 이런 종류의 프로그램을 멀티프로그래밍 이라고 함

 

 멀티 프로그래밍

  • 여러개의 프로그램이 동시에 실행되는 것 ( IO 작업이 발생하면 다른 프로세스가 CPU 에서 실행됨 ) 
  • 목적 : CPU 사용률을 극대화 시킴
  • 단점 : CPU 사용 시간이 길어지면 다른 프로세스는 계속 대기해야 함
  • 해결 : "프로세스는 한번 CPU 를 사용할 때 아주 짧은 시간만 CPU 에서 실행되도록 하자" 라는 아이디어. 이런 종류의 시스템을 멀티태스킹이라고 함

 

▶ 멀티 태스킹

  • CPU 타임을 아주 짧게 쪼개서 그 타임 안에 여러 프로세스들이 서로 번갈아 가면서 실행될 수 있도록 함
  • 목적 : 프로세스의 응답시간을 최소화함
  • 단점
    • 여러 프로세스가 동시에 실행되는 것은 시스템에서 지원하나, 하나의 프로세스가 동시에 여러 작업을 수행하지는 못함
    • 프로세스의 컨텍스트 스위칭 비용이 무겁다
    • 프로세스끼리 데이터 공유가 까다롭다
    • 듀얼코어가 등장했는데 잘 쓰고 싶다. 
  • 해결책 : 스레드

++ 멀티스레딩이 이후 멀티태스킹의 개념의 변화

멀티 태스킹 : 여러 프로세스여러 스레드가 아주 짧게 쪼개진 cpu time 을 나눠 갖는 것

 

듀얼코어 ?

  • 듀얼코어는 하나의 CPU 에 두 개의 독립적인 처리 유닛(Core) 이 내장된 PC. 
  • 듀얼코어는 프로세스나 스레드를 물리적으로 동시에 실행할 수 있어 병렬처리, 멀티태스킹, 멀티스레드에 유리하다. 
  • 코어 ? 명령어를 해석하고 실행하는 CPU의 실질적인 처리 장치
  • + 멀티 코어 : 2개 이상의 코어를 가진 CPU (듀얼, 쿼드, 헥사 등 모두 포함)

 

 스레드

  • 등장 배경 : 한 프로세스 내에서 여러개의 작업을 동시에 실행할 수 있게 하기 위함
  • 프로세스는 한 개 이상의 스레드를 가질 수 있다
  • 스레드는 CPU 에서 실행되는 단위이다(과거 = 프로세스가 CPU 에서 실행되는 단위 / 현재 = 스레드)
  • 같은 프로세스의 스레드들끼리 컨텍스트 스위칭은 가볍다
  • 스레드는 자신들이 속한 프로세스의 메모리 영역을 공유한다 + 데이터 공유가 쉽다 (그래서 컨텍스트 스위칭 비용이 쌈)
  • 그렇더라도 스레드들만의 고유한 영역도 있음 ! 

 

▶ 멀티스레딩

목적 : 하나의 프로세스가 동시에 여러 작업을 실행하는데 목적이 있음

  • 왼 ) CPU 한개가 있고, 두개의 스레드를 가지는 프로세스가 있다고 가정하면, 두개의 스레드는 아주 짧은 시간마다 스위칭 되며 CPU 를 사용하게 된다. 
  • 오 ) 코어가 두개인 CPU 한개가 있을 때, 스레드 두 개를 가지는 프로세스는 각각의 코어에서 병렬적으로 실행된다. 진정한 동시실행인것. 이런 종류의 시스템을 멀티스레딩이라고 함. 

 

▶ 멀티 프로세싱

  • 두 개 이상의 프로세서나 코어를 활용하여 동시에 여러 작업을 처리하는 방식
  • 각 프로세스가 독립된 메모리 공간을 사용한다. 

그럼 멀티 스레딩이랑 같은거 아닌가 ?

  • 멀티프로세싱은 독립적인 작업 단위를 병렬로 실행하고,
  • 멀티스레딩은 하나의 프로세스 내에서 여러 흐름을 동시에 수행한다. 
  • 멀티 프로세싱은 프로세스마다 독립된 메모리 공간을 가지고 있으므로 하나 죽어도 다른 프로세스에 영향이 없다. 또한 CPU 코어가 많을 수록 병렬 처리에 유리하다. 
  • 멀티 스레딩은 위치해 있는 프로세스 내부의 메모리 공간을 공유하고 있어 컨텍스트 스위칭이 효율적인 것이 장점이다. 

근데 ! 멀티스레딩이 컨텍스트 스위칭 비용이 싼 것이 장점이라고 했는데, 이게 정말 장점이 맞을까 ? 

멀티스레딩은 멀티코어에서 병렬로 실행되기도 하는데, 그렇다면 컨텍스트 스위칭이 적게 드는 게 진짜 장점이라고 할 수 있을까? → 그렇다고 할 수 있음 ↓

멀티코어 환경에서 멀티스레딩이라면 컨텍스트 스위칭은 오히려 적게 발생합니다.
하지만 멀티스레딩이라도 코어 수보다 스레드 수가 많으면 여전히 스위칭이 필요하고,
이때 스레드 간 스위칭은 프로세스 간보다 비용이 적기 때문에 “효율적이다”라고 말할 수 있습니다.

 

+ 기본적으로 멀티 프로세싱은 프로세스마다 독립된 메모리 공간을 사용하여 기본적으로 메모리를 공유할 수 없으나, Python 에서 SharedMemory 등을 사용하여 공유 메모리를 사용할 수 있다. 

 

 

※ 언어별 멀티스레딩과 멀티프로세싱

▶ JAVA

 

JAVA 멀티스레딩

  • JAVA는 스레드 기반 설계가 매우 강력한 언어임. 
  • Thread 클래스 또는 Runnable 인터페이스로 사용 가능. 

Q ) 그럼 자바는 단일 코어에서 멀티스레딩 코드를 어떻게 처리할까 ?

A ) Java는 단일 코어에서도 멀티스레딩 코드를 "병렬처럼 보이게" 실행한다. 아주 빠르게 context switching 이 이뤄지며 동시성을 제공하는 것 ! + 근데 요즘은 거의 다 멀티코어 CPU 를 지원함

 

 

JAVA 멀티프로세싱

  • Java 자체는 멀티프로세싱을 직접 지원하진 않음
  • 그렇지만 ProcessBuilder 로 외부 프로세스를 실행하거나
  • 여러 JVM 프로세스를 띄워서 병렬 작업을 수행할 수 있음

 

▶ Python

 

Python 멀티스레딩

  • Python의 threading 모듈로 구현 가능하지만, GIL (Global Interpreter Lock) 때문에 동시에 하나의 스레드만 실행
  • CPU 바운드 작업 에서는 성능 향상 거의 없지만,  IO 바운드 작업(파일 다운로드, 웹 크롤링 등)에는 효과적임

아래 코드를 보면, 스레드가 2개이지만 실제로는 하나씩 교대로 실행된다. 시간은 싱글스레드와 동일하거나, 느리다. 

import threading

def count():
    x = 0
    for _ in range(10**7):
        x += 1

t1 = threading.Thread(target=count)
t2 = threading.Thread(target=count)

t1.start()
t2.start()
t1.join()
t2.join()

 

 

GIL ?

  • Global Interpreter Lock
  • 멀티스레딩을 하더라도 동시에 하나의 스레드만 실행되도록 제한하는 전역 락(Global Lock) 임
  • 목적 : CPython 내부의 많은 객체들이 레퍼런스 카운팅 기반으로 메모리를 관리하는데, 이 구조는 멀티스레드 환경에서 동기화 문제가 발생할 가능성이 있다. 그래서 GIL 이라는 락을 만들어 동시에 하나의 스레드만 실행하게 함
  • 해결 : multiprocessing
    • Python 에서는 GIL 때문에 멀티스레딩으로는 병렬 처리에 한계가 있어, 진짜 병렬 처리가 필요할 땐 multiprocessing 모듈을 사용한다. 

 

Python 멀티프로세싱

  • multiprocessing 모듈 사용 시, GIL 무력화 가능하여 진짜 병렬 처리가 가능해짐
  • CPU 바운드 작업은 반드시 multiprocessing 써야 한다. >> 왜 ?
    • ㄴ CPU 바운드 작업은 연산이 많아 CPU 를 오래 붙잡는 작업이 많다. multiprocessing 은 프로세스마다 GIL 이 따로 존재하여, 코어 수대로 병렬 활용이 가능하다. 하지만 스레드를 쓰면 스레드는 GIL 때문에 스레드가 동시에 실행되지 못하고, 하나씩 switching 되며 실행되어 CPU를 오래 붙잡고 있을 수가 없다. 
✔️ 파이썬에서
CPU 병렬 작업은 멀티프로세싱, IO 병렬 작업은 멀티스레딩을 선택하는 게 정석입니다.

 

 

Python 멀티프로세싱의 공유메모리

  • 공유메모리 : 운영체제가 하나의 물리적 메모리 영역을 생성하고, 여러 프로세스가 그 메모리 영역을 함께 매핑해서 사용하는 방식
  • 목적 : 멀티프로세싱에서는 각 프로세스가 독립된 주소 공간을 가지기 때문에 서로의 메모리 공간에 직접 접근할 수 없다. 하지만 프로세스 간 데이터 교환이 필요하고 빠르게 대량의 데이터를 주고 받아야 할 때 공유메모리 방법을 사용한다.
  • 종류
    • shared_memory : 저수준 메모리 공유 (바이트 수준의 진짜 공유 메모리)
      Manager().list() : 고수준 공유 객체 (list, dict 등 Python 객체 공유)
  • 단점 : 동기화
    • 공유 메모리는 여러 프로세스가 동시에 접근 할 수 있기 때문에, 동기화가 없으면 데이터 충돌이 발생한다. 

동기화 ? 

  • 동기화는 동시에 접근하는 것을 제어하여 한 번에 하나만 공유 자원에 접근하도록 하는 방법. 
  • 해결방법으로는 multiprocessing.Lock, Manager().list()을 사용하는 방법이 있다. 
  • 공유 메모리의 단점인 동시 접근 문제는 반드시 락(lock) 등 동기화 기법으로 해결해야한다. 
    • ex ) 두 프로세스가 동시에 공유 메모리에 write 하는 상황 (이런 상황을 경쟁상태 Race Condition 라고 한다. 결과는 프로세스가 누가 먼저 쓰느냐에 따라 달라지며, 예측이 불가능해진다.)
초기값: 100

프로세스 A가 읽음 → 100
프로세스 B도 동시에 읽음 → 100

A가 +1 계산하고 씀 → 101
B도 자기 +1 계산한 값(100+1)을 씀 → 101 ❗

→ 실제로는 두 번 증가시켰는데 결과는 +1만 적용됨 (원래는 102가 되어야)

 

 

 

▶ Node.js

Node js 의 멀티 스레딩

Node.js 는 기본적으로 싱글스레드 모델이지만, worker_threads를 사용하면 제한적인 멀티스레드 프로그래밍이 가능하다. 

 

그럼 Node.js 는 무거운 CPU 바운드 작업은 어떻게 처리하지 ?

Node.js 는 싱글 스레드 기반 이벤트 루프 구조라서, 무거운 CPU 작업을 그대로 실행하면 전체 서비스가 멈춰버릴 수도 있다.

그래서 Node.js 환경에서는 worker_threads 모듈을 사용하는 등의 전략으로 CPU 바운드 작업을 분리하거나 병렬처리한다. 

 

Node.js 의 멀티 프로세싱

  • Node.js는 싱글 스레드지만, 멀티프로세싱으로 병렬 처리가 가능하다. 
    • child_process.fork() : 일반 작업 병렬 처리
    • cluster : 하나의 서버(Node.js 앱)를 여러 개의 프로세스로 실행해 부하 분산
  • CPU 코어 수만큼 병렬 처리 가능
  • 각 프로세스는 별도 메모리 공간 사용