티스토리 뷰
이 개념이 속도 및 성능 개선에 중요한 포인트라고 한다.
우선 의미를 동기 = 순서, 스레드 = 공간(일꾼) 이라고 잡고 시작한다.
01 동기 vs. 비동기
- 동기: 순차적으로 실행되는 것
- 비동기: 요청을 받은 뒤 먼저 작업이 끝난 순으로 실행되는 것
02 프로세스 vs. 스레드
프로세스 (Process)
- 실행 중인 프로그램의 기본 단위
- 독립된 메모리 공간을 가짐 → 프로세스 간에는 메모리를 공유하지 않음
- 컨텍스트 스위칭(heavyweight context switching)이 필요 → (메모리를 공유하지 않기 때문에) 전환 비용이 큼
스레드 (Thread)
- 하나의 프로세스 내에서 실행되는 작은 실행 단위
- 같은 프로세스 내에서는 메모리를 공유 → 데이터 공유가 용이 [여러 스레드가 같은 데이터(변수, 객체 등)에 접근 가능]
- 컨텍스트 스위칭이 적음(lightweight context switching)
스레드는 CPU 수행의 기본 단위 또는 프로세스 안의 제어권의 흐름이다. 스레드가 수행되는 환경을 Task라고 부르는데, 전통적인 프로세스는 하나의 스레드가 있는 Task와 일치한다.
03 문맥 교환 (Context Switching)
컴퓨터에서 동시에 처리할 수 있는 최대 작업 수는 CPU의 코어 수와 같다. 만약, CPU의 코어 수보다 더 많은 스레드가 실행되면, 각 코어가 정해진 시간 동안 여러 작업을 번갈아가며 수행하게 된다.
이때, 각 스레드가 서로 교체될 때 스레드 간의 문맥 교환이라는 것이 발생한다. 문맥 교환이란, 현재까지의 작업 상태나 다음 작업에 필요한 각종 데이터를 저장하고 읽어오는 작업을 말한다. 즉, 현재 상태를 저장하고 새로운 프로세스를 실행하는 과정이다.
04 멀티 스레드 vs. 단일(싱글) 스레드
일반적으로 하나의 프로세스는 하나의 스레드를 가지고 작업을 수행한다. 하지만, 멀티 스레드란 하나의 프로세스 내에서 둘 이상의 스레드가 동시에 작업을 수행하는 것을 의미한다. 또한, 멀티 프로세스는 여러 개의 CPU를 사용하여 여러 프로세스를 동시에 수행하는 것을 의미한다.
구분 | 멀티 프로세싱 (Multi-processing) | 멀티 스레딩 (Multi-threading) |
실행 방식 | 여러 개의 독립적인 프로세스가 동작 | 하나의 프로세스 내에서 여러 스레드가 동작 |
메모리 공유 | 프로세스별 독립적인 메모리 공간 사용 | 같은 프로세스 내에서 메모리 공유 |
실행 단위 | 프로세스 | 스레드 |
I/O 작업 | 비효율적 | 유리 (I/O가 많은 작업에 적합) |
CPU 사용 | 유리 (병렬 계산 가능) | 불리 (GIL의 영향을 받음) |
* I/O 작업: Input/Output 입출력 작업. 컴퓨터가 외부 장치(키보드, 마우스, 하드디스크 등)와 데이터를 주고받는 작업
* GIL(Global Interpreter Lock): Python에서는 한 번에 하나의 스레드만 실행되도록 하는 메커니즘. CPU 연산이 많은 작업에서는 멀티 프로세싱이 더 유리.
05 멀티 스레딩이 유용한 경우
멀티 프로세싱이 효과적인 작업
: 대규모 데이터 연산과 같은 CPU 내부에서 연산이 많은 경우 (CPU Bound)
프로세스 내의 스레드는 모두 각각 독립적인 실행 파일이며, 모든 스레드는 프로세스의 일부이다. 프로세스를 여러 개 수행해도 되지만 굳이 스레드를 사용하는 이유는 다음과 같다.
- 프로세스를 생성하거나 Context switching 하는 작업은 너무 무겁고 잦으면 성능 저하가 발생하는데, 스레드를 생성하거나 switching 하는 것은 그에 비해 가볍다.
- 두 프로세스가 하나의 데이터를 공유하려면 메시지 패싱이나 공유 메모리 또는 파이프를 사용해야 하는데, 이는 효율도 떨어지고 개발자가 구현, 관리하기도 번거롭다.
멀티 스레딩이 특히 효과적인 작업
: 네트워크 요청 처리와 같은 CPU 바깥에서 일어나는 작업이 많은 경우 (I/O Bound)
멀티 스레드는 각 스레드가 자신이 속한 프로세스의 메모리를 공유하므로, 시스템의 자원 낭비가 적다. 또한, 하나의 스레드가 작업을 할 때 다른 스레드가 별도의 작업을 할 수 있어 사용자와의 응답성도 좋아진다.
문맥 교환이 걸리는 시간이 커지면 커질수록, 멀티 스레딩의 효율은 저하된다. 오히려 많은 양의 단순한 계산은 싱글 스레드로 동작하는 것이 더 효율적일 수 있다. 따라서, 많은 수의 스레드를 실행하는 것이 언제나 좋은 성능을 보이는 것은 아니라는 점을 유의해야한다.
06 동시 실행 vs. 병렬 실행
동시 실행 (Concurrency)
- 한 프로세스에서 여러 스레드를 실행하는 것
- CPU가 빠르게 스레드 간을 전환하면서 동시에 실행되는 것처럼 보이게 함
- ex. 웹 브라우저에서 여러 탭을 띄워두고 사용할 때
병렬 실행 (Parallelism)
- 여러 개의 프로세스를 동시에 실행하는 것
- 다중 CPU가 필요 → 병렬로 여러 작업을 수행 가능
- ex. 데이터 과학에서 대량 연산을 여러 개의 CPU에서 동시에 수행하는 경우
프로세서가 여러 개인 경우, 멀티 스레드를 통해 병렬성(Parallelism)을 높일 수 있다. 즉, 여러 작업이 동시에 수행될 수 있다. 이는 프로세스의 스레드들이 각각 다른 프로세서에서 병렬적으로 수행될 수 있기 때문이다. 병렬성은 CPU의 개수에 비례한다.
만약 프로세서가 하나인 경우엔, 멀티 스레드를 통해 동시성(Concurrency)을 높일 수 있다. 실제로는 각각의 시간에 한 작업만 수행되지만, 병렬적으로 수행되는 것처럼 보이는 것이다. 만약 한 스레드가 blocked(waiting) 되더라도 커널이 다른 스레드로 switch 시켜 실행할 수 있어서, 하나의 프로세서임에도 불구하고 빠른 처리가 가능하고 계산 속도가 증가한다.
결론
그래서 결론적으로, 이렇게 좋은 멀티스레드가 실제 개발환경에서는 어떤 영향을 주는 것인가 ? 지피티와 대화를 통해 그 연결성을 찾아보았다.
우선 위에서 언급했던 멀티 스레드의 여러 장점들을 정리해보자면,
- CPU 활용 최적화: 여러 개의 작업을 동시에 처리 가능하다.
- I/O 작업 최적화: 한 스레드가 I/O 작업을 기다리는 동안 다른 스레드가 CPU를 사용할 수 있어 낭비가 줄어든다.
- 응답 속도 개선: GUI 환경에서 하나의 스레드가 사용자 입력을 받는 동안 다른 스레드가 백그라운드 작업을 수행할 수 있다.
개발할때는 어떻게 활용되는지 ?
- 웹 서버 요청 처리: 여러 요청을 동시에 처리하여 성능 개선
- 대량 데이터 처리 (크롤링, 데이터베이스 작업): 여러 개의 스레드를 사용하여 빠르게 처리 가능
파이썬 예제 코드
def print_sum(num1, num2):
time.sleep(3)
print(num1 + num2, time.ctime())
1. concurrent.futures 모듈을 이용한 멀티스레딩 ⇒ 여러 작업을 동시에 실행하는 효과를 확인
import time
from concurrent.futures import ThreadPoolExecutor
def main():
with ThreadPoolExecutor(max_workers=3) as executor: # 최대 3개의 스레드 생성
executor.submit(print_sum, 1, 2) # submit()로 print_sum 함수를 비동기적으로 실행
executor.submit(print_sum, 2, 3)
executor.submit(print_sum, 3, 4)
print("done!")
main()
- 스레드 풀(Thread Pool): 스레드를 미리 생성해두고 필요할 때 작업을 할당하여 실행하는 방식. 효율적으로 리소스를 관리할 수 있다.
- submit(): 비동기적으로 함수를 실행할 때 사용
- with 구문: ThreadPoolExecutor가 자동으로 종료되도록 관리
2. multithreading 모듈을 사용한 멀티스레딩 (두 가지 버전의 예제) ⇒ 수동, 세밀한 제어 가능
import threading
import time
def main2():
thread1 = threading.Thread(target=print_sum, args=(1,2))
thread2 = threading.Thread(target=print_sum, args=(2,3))
thread3 = threading.Thread(target=print_sum, args=(3,4))
thread1.start()
thread2.start()
thread3.start()
time.sleep(4)
print("done")
main2()
- threading.Thread(target=print_sum, args=(1,2)) 를 이용해 새로운 스레드를 생성하고 실행
- thread1.start(), thread2.start(), thread3.start()로 각 스레드 시작
def main3():
thread1 = threading.Thread(target=print_sum, args=(1,2))
thread2 = threading.Thread(target=print_sum, args=(2,3))
thread3 = threading.Thread(target=print_sum, args=(3,4))
thread1.start()
thread2.start()
thread3.start()
main_thread = threading.currentThread()
for thread in threading.enumerate():
if thread is main_thread:
continue
thread.join()
print(thread.name, thread.is_alive())
print("done")
for thread in threading.enumerate():
print(thread.name, thread.is_alive())
main3()
- threading.Thread()로 3개의 스레드를 생성하고 실행
- threading.enumerate() 를 이용해 현재 실행 중인 모든 스레드를 확인
- 메인 스레드를 제외한 나머지 스레드의 종료를 기다림 (join())
- 각 스레드가 종료될 때마다 thread.is_alive() 값을 출력
3. multiprocess을 사용한 병렬 처리
from concurrent.futures import ProcessPoolExecutor
def main4():
with ProcessPoolExecutor(max_workers=3) as executor: # 3개의 프로세스를 생성
executor.submit(print_sum, 1, 2) # 각 프로세스를 비동기적으로 실행
executor.submit(print_sum, 2, 3)
executor.submit(print_sum, 3, 4)
print("done!")
main4()
- 멀티프로세싱(Multiprocessing): 프로세스를 여러 개 만들어 CPU 코어를 병렬로 사용할 수 있게 해줌.
- ProcessPoolExecutor: 프로세스 풀을 생성하여 병렬 실행을 효율적으로 관리
- GIL 회피: 스레드 기반 멀티태스킹은 GIL(Global Interpreter Lock)의 영향을 받지만, 프로세스 기반 멀티태스킹은 GIL 영향을 받지 않는다.
Reference
https://dgjinsu.tistory.com/30
https://www.tcpschool.com/java/java_thread_multi
'kakao tech bootcamp > 딥다이브' 카테고리의 다른 글
도커, 컨테이너, 쿠버네티스 [진행중] (0) | 2025.02.17 |
---|---|
데이터 분석 전에 수행해야 하는 전처리 과정과 데이터 품질을 향상시키는 방법 (0) | 2025.02.14 |
대수 구조와 선형대수 (0) | 2025.02.08 |
Pandas의 결측치 처리 기능이 데이터 정리에 주는 이점 (0) | 2025.02.08 |
파이썬 기초: 조건문/반복문/순서도/예외 처리/재귀 함수 (0) | 2025.02.08 |