티스토리 뷰
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 리소스가 낭비된다) 비용 등
'AI > Python' 카테고리의 다른 글
[Python] *args, **kwargs (0) | 2025.02.09 |
---|---|
[Python] 비동기 asyncio, async, 이벤트루프, 코루틴(Coroutine) (0) | 2025.02.07 |
[Python] if문과 with문에서의 변수와 리소스, Requests (0) | 2025.02.03 |
[python] 우선순위 큐(Priority Queue)와 힙(Heap) (1) | 2024.12.27 |
[python] 스택을 이용한 주식가격 문제풀이 (1) | 2024.12.26 |
- Total
- Today
- Yesterday
- ChatGPT
- 루틴
- 줄넘기
- 빅데이터 분석기사
- Python
- 영어회화
- 다이어트
- 스크랩
- 실기
- 뉴스
- 아침운동
- 미라클모닝
- 프로그래머스
- 티스토리챌린지
- Ai
- 운동
- 고득점 Kit
- C언어
- 습관
- 경제
- 아침
- 30분
- llm
- 오블완
- IH
- 기초
- 오픽
- SQL
- opic
- 갓생
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |