티스토리 뷰

1. GIL이란?

- Global Interpreter Lock

- Python의 CPython 인터프리터에서 동시에 하나의 스레드만 실행될 수 있도록 제한하는메커니즘

- 멀티스레드 환경에서도 한 번에 하나의 스레드만 Python 바이트 코드를 실행할 수 있다.

- 따라서 멀티스레딩으로 처리하면 느려질 수 있다.

 

2. GIL이 왜 필요할까?

- Python의  메모리 안전성을 유지하기 위해 존재

- Python이 메모리 관리 시스템은 참조 카운트를 기반으로 한다

- 여러 개의 스레드가 동시에 실행되면 참조 카운트 값을 수정하는 과정에서 Race Condition이 발생할 수 있는데, 이를 방지하기 위함이다

* Race Condition 이란?

- 두 개 이상의 스레드가 동시에 같은 자원(메모리, 파일, 변수 등)에 접근하면서 예기치 않은 결과가 발생하는 상황

 

3. GIL의 영향

- 멀티코어 CPU 환경에서 병렬 처리 성능을 제한할 수 있다.

- GIL로 인해 Python의 멀티 스레딩은 CPU를 100% 활용하지 못한다

- CPU 바운드 작업(연산-intensive 작업)에서는 멀티스레딩보다 멀티프로세싱이 더 효율적이다

- I/O 작업에서는 GIL의 영향이 적다

- I/O 바운드 작업(파일 입출력, 네트워크 요청 등)을 수행하는 동안 GIL이 다른 스레드에 양보되므로 성능 저하가 크지 않다. 따라서 threading 모듈을 사용한 멀티스레딩은 I/O 바운드 작업에서는 유용할 수 있다.

 

4. GIL 영향을 받지 않는 방법

1) 멀티프로레싱

- 여러 개의 프로세스를 생성하여 실행하며, 각 프로세스는 독립적인 메모리 공간을 가진다.

- GIL의 영향을 받지 않고 CPU 코어를 100% 활용할 수 있다

- CPU 연산이 많은 작업에서는 multiprocessing 모듈을 활용하는 것이 더 좋다

2) NumPy, Cython, Numba 사용

- C기반으로 구현된 라이브러리는 내부적으로 GIL을 해제할 수있다. 

3) PyPy

- JIT(Just-In-Time) 컴파일러를 사용하여 GIL의 영향을 줄일 수 있다.

 

5. 언어별 메모리 관리

언어 특징 장점 단점
Python 참조카운트 + GC 개발자가 직접 malloc()또는 free()같은 메모리 할당/해제를 하지 않아도 된다 GIL 때문에멀티코어 CPU를 제대로 활용할 수 없다
C, C++ 수동 메모리관리 불필요한 메모리 사용을 줄일 수 있다
GIL같은 락이 필요하지 않아 멀티 스레딩 성능이 뛰어나다
메모리 누수가 발생할 수 있다.
개발자가 실수할 수 있다.
Java 가비지 컬렉터(GC) 기반 GC가 자동으로 메모리를 관리하며, 멀티스레딩 가능 GC가 실행될 때 멈칫하는 문제(Stop-the World 현상)가 발생할 수 있다
GIL은없지만 멀티스레드에서 동기화가 필요한 경우 synchronized 키워드 필요

 

6. 참조카운트란?

- 객체가 몇 개의 변수 또는 다른 객체에 의해 참조되고 있는지를 나타내는 값

- 객체의 참조 카운트가 0이 되면 Python은 자동으로 해당 객체를 삭제(메모리 해제)한다

- 참조 카운트를 정확하게 관리해야 메모리 누수나 잘못된 메모리 접근이 발생하지 않는다

 

7. Python의 멀티스레딩과 참조 카운트 문제

- Python에서 여러 개의 스레드가 동시에 실행되면 하나의 객체를 여러 스레드가 동시에 참조할 수 있다

- 두 스레드가 같은 객체 x를 참조하고 있다고 가정할 때, 두 스레드가 동시에 x의참조 카운트를 증가시키면 각 스레드는 원래 참조 카운트가 1이라고 보고 각각 2로 증가시키면서 스레드간 충돌로 인해 참조 카운트가 올바르게 증가하지 않게된다.

[Thread 1]  x의 참조 카운트: 1 → 2  (읽고, 증가)
[Thread 2]  x의 참조 카운트: 1 → 2  (읽고, 증가)

- GIL은 이런 문제가 발생하지 않도록 보호하는 역할을 한다

- GIL은 한 번에 하나의 스레드만 실행되도록 제한해, 여러 스레드가 존재하더라도 동시에 python 바이트코드를 실행하는 것은 하나의 스레드만 가능하다(4개의 CPU 코어가 있더라도, Python 스레드 하나만 실행된다)

=> 스레드 간 참조 카운트 접근을 직렬화(serialize)한다. 즉, 한 스레드가 객체의 참조 카운트를 변경하는 동안 다른 스레드는 대기하도록 만든다. 따라서 Race Condition이 발생하지 않고, 참조 카운트가 안전하게 관리되어 메모리 안정성을 유지할 수 있다.

 

8. 직렬화

- 직렬화: 객체 또는 데이터를 저장하거나 전송하기 위해 하나의 연속적인 형태로 변환하는 과정

- 역직렬화 : 직렬화된 데이터를 원래 객체 형태로 복원하는 과정

1) 직렬화가 필요한 이유

- 객체를 파일에 저장할 때, 객체를 다른 컴퓨터로 보낼 때(네트워크 전송), 데이터베이스 저장

- Python에서 직렬화 예시 : pickle사용으로 python 객체를 바이트 스트림으로 직렬화하고, 다시 객체로 변환(역직렬화)

import pickle

data = {"name": "Alice", "age": 25}

# 데이터 직렬화
with open("data.pkl", "wb") as f:
    pickle.dump(data, f)

# 데이터 역직렬화
with open("data.pkl", "rb") as f:
    loaded_data = pickle.load(f)

print(loaded_data)  # {'name': 'Alice', 'age': 25}

 

9. 멀티프로세싱은 왜 참조 카운트 문제가 발생하지 않을까?

- multiprocessing 모듈을 사용하면 각 프로세스가 독립적인 Python 인터프리터를 실행하므로 GIL의 영향을 받지 않는다

- 각 프로세스는 독립된 메모리 공간을 가지기 때문에 참조 카운트 문제가 발생하지 않는다.

 

* IPC(Inter Process Communication, 프로세스 간 통신)

- 여러 개의 독립적인 프로세스들이 데이터를 주고 받을 수 있도록 하는 방법

- Python의 multiprocessing 모듈을 사용할 때 각 프로세스는 독립적인 메모리 공간을 가지기 때문에 직접 변수를 공유할 수 없다(메모리 공유 안됨) . 따라서 프로세스 간 데이터를 교호나하려면 IPC 방법이 필요하다

IPC 방법 설명
multiprocessing.Queue 큐(Queue)를 사용하여 프로세스 간 데이터를 전달
multiprocessing.Pipe 두 개의 프로세스를 연결하여 데이터 송수신 가능
multiprocessing.Manager().dict() 공유 메모리를 제공하여 데이터를 저장 가능
multiprocessing.Value 공유 변수를 사용하여 값을 저장 가능

 

10. Java의 GC와 Python의 GC 차이점

- GC: 가비지 컬렉터(자동 메모리 관리)

 1) Java의 GC 실행방식

- 객체가 사용 중인지 검사 후, 사용되지 않는 객체를 삭제

- 객체를 새로운 객체와 오래된 객체로 나눠서 관리하며, 오래된 객체는 GC를 덜 실행해 성능을 최적화 한다

- 백그라운드에서 주기적으로 실행되어 개발자가 GC를 신경쓰지 않아도 자동으로 메모리가 관리된다

 

2) Python의 GC

- Python에서는 참조 카운트가 0이 되면 즉시 객체가 삭제되므로, 기본적으로 GC가 필요 없다

- 하지만 순환 참조가 발생하면 참조 카운트가 0이 되지 않아서 객체가 삭제되지 않는다

- python의 GC는 주기적으로 순환 참조를 감지하여 자동으로 삭제한다.

- python은 객체를 세대 단위로 관리하며, 세대가 쌓일수록 GC가 실행되고 순환 참조를 감지하면 자동으로 삭제한다.

- GC 실행 조건이 충족되지 않거나 Python이 실행되는 시간이 짧아 GC가 실행되기 전에 프로그램이 종료되면, 순환 참조 객체가 메모리에 남아있을수도 있다. 이럴 경우 개발자가 gc.collect를 사용하여 가비지 컬렉션을 수동으로 실행할 수 있다.

 

11. 왜 I/O 작업은 GIL의 영향을 받지 않을까?

- GIL은 Python 바이트코드를 실행하는 동안에는 한 번에 하나의 스레드만 실행 되도록 제한하는데, I/O 작업(파일 읽기/쓰기, 네트워크 요청, 데이터베이스 쿼리 등)은 Python 바이트 코드를 실행하는 것이 아니라, 운영 체제(OS)에서 처리하기 때문에 I/O 작업 중에는 python 스레드가 GIL을 해제하고, 다른 스레드가 실행될 수 있어 GIL의 영향을 받지 않는다.

* requests.get() 실행 중에는 python이 아니라 운영체제(OS)의 네트워크 스택이 HTTP 요청을 처리한다.

 

12. Python I/O 작업 라이브러리(GIL해제, 운영체제에서 실행)

작업 유형 라이브러리/함수 설명
파일 입출력 open() 파일 입출력
csv csv 파일 읽기
shutil 파일복사
zipfile zip 파일 압축
tarfile TAR 파일 압축
네트워크 요청 socket 네트워크 통신
requests HTTP 요청
urllib HTTP 요청
http.client HTTP 요청
aiohttp 비동기 HTTP 요청
데이터베이스 pymysql MySQL 데이터베이스
psycopg2 PostgreSQL 데이터베이스
sqlite3 SQLite 데이터베이스
타이머 time.sleep() 타이머(대기)
외부 프로세스 실행 subproocess 외부 프로세스 실행
multiprocessing 병렬 프로세스
비동기 asyncio 비동기 I/O
selectors 비동기 I/O 다중 처리

 

13. 비동기(ayncs) vs 멀티스레딩(threading)

- 웹 스크래핑과 같은 I/O 바운드 작업을 처리할 때, 뭐가 더 좋을까?

- 비동기는 요청이 많고 대규모 작업을 할 때, 멀티스레딩은 소규모 작업에 적합

 

1) 비동기 방식: aiohttp

- asyncio.gather()를 사용해 여러 개의 요청을 동시에 실행

- 싱글스레드이므로 스레드 오버헤드가 없다. 메모리를 적게 사용하고 성능이 더 좋다.

- 대량의요청을 빠르게 처리할 수 있다.

* 아래 예제에서는 ClientSession을 한 번만 생성하고 재사용 하기 때문에 불필요한 메모리 사용이 감소하고, 싱글 스레드에서 모든 요청을 처리하므로 메모리 사용량이 낮다. 스레드 오버헤드 없이 요청을 동시에 실행 가능하다.

** 세션이란? 하나의 연결을 유지하면서 여러 요청을 처리하는 객체이다. 즉, 같은 서버에 여러 개의 요청을 보낼 때, 매번 새로운 TCP 연결을 만드는 것이 아니라 기존 연결을 재 사용하는 방식이다.

*** TCP 연결이란? 두 개의 컴퓨터(클라이언트와 서버)가 네트워크를 통해 데이터를 안저적으로 주고 받을 수 있도록 설정하는 연결 방식. 즉, 인터넷에서 데이터를 신뢰성 있게 전송하는 표준 프로토콜

import asyncio
import aiohttp

urls = ["https://example.com"] * 5  # 여러 개의 URL 요청

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, url) for url in urls]
        responses = await asyncio.gather(*tasks)
        print("All responses received!")

asyncio.run(main())

 

2) 멀티스레딩 : threading

- 일반적인 python 코드처럼 작성 가능해 이해하기 쉽다

- 스레드마다 메모리를 추가로 사용하므로, 대량의 요청을보낼 때 메모리 사용량이 등가한다.

- 스레드 오버헤드가 있어서 비동기 방식보다 성능이 떨어질 수 있다.

* 아래 예제에서, requests.get(url)을 실행할 때마다 새로운 HTTP 연결을 생성하는데, 각 스레드는 독립적으로 실행되므로 중복된 TCP 연결이 많아져 메모리 사용량이 증가한다.

- 스레드가 많아질 수록 메모리 낭비 + 컨테스트 스위칭 비용이 증가한다.

import threading
import requests

urls = ["https://example.com"] * 5  # 여러 개의 URL 요청

def fetch(url):
    response = requests.get(url)
    print(f"Downloaded {url}")

threads = []
for url in urls:
    t = threading.Thread(target=fetch, args=(url,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

 

14. 스레드 오버헤드란?

- 멀티스레드를 사용할 때 추가적으로 발생하는 비용(메모리 할당, 스레드 스케줄링 작업 등)

- 스레드를 생성하고 관리하는데 필요한 CPU, 메모리, 컨텍스트 스위칭(CPU가 여러 개의 스레드를 번갈아 실행하려면 각 스레드의 상태를 저장하고 복원하는 과정이 필요하다, 이 과정에서 CPU 리소스가 낭비된다) 비용 등

 

반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
글 보관함