python) 동시성, 병렬성, 멀티 스레드 그리고 GIL

렁호·2023년 7월 3일
0
post-thumbnail

0. 개요

하이퍼 스레드의 시대에 살고 있는 우리에게 컴퓨터의 실행 단위는 이제 더이상 프로세스가 아니다. 스레드 단위의 작업 수행으로 우리는 보다 효율적으로 리소스를 사용하고 작업의 효율성을 높일 수 있다.

하지만 이런 시대에서도 우리의 python 만큼은 그 행태를 달리한다.
많은 사람들은 python의 멀테 스레드는 멀테 스레드가 아니라고 하는데 왜 그런지 알아보자.

사실 python 개발자들의 단골 블로그 포스팅 토픽이라 굳이 정리해야되나 싶었지만 나만의 시각으로 한번 다뤄보고 싶어서 적어본다.

1. 동시성과 병렬성

우선 python의 멀티스레드가 어쩌구 저쩌구 하기 전에 동시성과 병렬성의 차이를 알아야할 필요가 있다.

동시성

병렬성 보단 동시성에서 많은 사람들이 혼동을 겪는다. 아마도 그 원인은 애매모호한 번역 때문이지 않을까 싶다.
동시성이라는 말의 뜻만 보면 '동시에 동작한다' 라고 이해하기 쉽지만 실제로는 그렇지 않다. 정확히는 '동시에 동작하는 것처럼 동작한다'가 맞는 표현이다.

CPU 관점으로 생각하면 보다 이해가 쉽다.
한 명의 요리사를 CPU라고 생각해보자, 이 요리사가 한 순간에 할 수 있는 요리 작업은 하나 일 것이다. 밥을 볶거나, 면을 삶거나. 하지만 이 작업들을 빠르게 번걸아가면서 진행하게 되면 두 가지 요리를 동시에 하는 것처럼 보인다. 하지만 분명 요리사는 한 순간에 한가지의 작업만 하고 있다는 사실을 명심하자.

이것이 동시성이다.

만약 한가지의 작업만 하고 있다는 게 이해가 잘 되지 않는다면 밑의 병렬성 예시를 보자.

병렬성

병렬성은 말 그대로 여러가지 일이 평행하게 동시에 진행 되고 있는 상태를 말한다.

위의 요리사 예를 빌려와서 설명해보자면, 요리사가 한 명 더 추가로 붙어서 한 명은 밥을 볶고 한명은 면을 삶는다. 동시에 두 가지의 요리가 만들어 지고 있으며, 여러 작업이 함께 이뤄지고 있다.

이게 병렬성이다.

정리

병렬성은 여러 CPU가 동시에 여러가지의 작업을 수행하는 것을 말하며, 동시성은 하나 또는 여러 CPU가 빠르게 작업을 교체해가며 마치 동시에 처리하는 것처럼 보이게 하는 것이다.

더 깊은 내용이 많으나 주제를 벗어나게 됨으로 이 차이만 이해하고 python의 멀티스레드가 어떻게 동작하는지 알아보자.

2. python 멀티스레드

멀티스레드 != 멀티스레드

분명히 python에는 멀티 스레드가 존재한다. 대표적으로 concurrent.futures, threading 등등의 내장 모듈이 있다. 그런데 왜 많은 사람들이 python의 멀티스레드를 부정하는 것일까 ?

그 이유는 python 멀티 스레드로는 병렬성을 구현할 수 없기 때문이다.
위의 동시성과 병렬성에 대한 내용을 봐서 알겠지만 멀티코어(요리사가 여러명)가 일반적인 시대에서 가능하다면 병렬로 처리하는 것이 효율면에서 우월할 것이다.
그러나 python에서는 GIL 시스템 덕분에 CPU 코어(요리사)가 아무리 많아도 동시성 처리밖에 할 수 없다.

GIL 이란

GIL이란 놈은 뭐길래 왜 요리사가 여러명 있어도 한 명만 일하게 만드는 것일까 ?
'Global Interpreter Lock' 의 약자인 GIL은 말 그대로 전역 인터프리터 잠금 장치라고 보면 된다.

python의 코드가 실행되는 동안 일관성 있게 유지되어야 하는 데이터가 존재한다. 이때 다른 스레드가 제어권을 가져와서 interrupt 하게 된다면 일관성이 깨질 수 있다.
그렇기에 GIL은 근본적으로 다른 스레드가 선점형으로 CPU의 제어권을 점유할 수 없도록 스레드가 진행되는 동안 LOCK을 걸어 일관성을 보장한다.

GIL이 보장하는 일관성 (GC)

그렇다면 GIL이 보장한다는 일관성이란 무엇을 의미하는 것일까,
일반적으로 여기서 말하는 일관성은 메모리의 상태라고 이해하는 것이 편하다. 정확히는 힙의 참조 횟수 변수이다.

모든 것이 객체인 python에서는 힙에 많은 양의 데이터가 생성 되고 GC(Garbage Collector)가 참조 횟수를 관리하고 참조 횟수가 0일 때 해당 메모리를 릴리즈 하는 방식으로 메모리를 관리한다.

여러 스레드가 동시에 작업할 경우 힙 영역을 공유하는 스레드 특성상 참조 횟수 변수에 동시에 접근하여 경쟁 상태가 발생하여 참조 횟수의 일관성 있는 상태를 오염시킬 수가 있는 것이다.

GIL은 완벽한가

우리는 멀티 스레드를 사용했을 때 모든 데이터의 일관성을 확실히 보장 받을 수 있을까 ?
암담한 결과지만 그렇지 않다.
GIL은 기본적으로 동시성 처리를 위해 여러 스레드를 최대한 공평하게 Context Switching 하며 처리하는데 이때 실행중이던 작업의 완료를 보장하지 않는다.

예를 들어서 두개의 스레드가 하나의 변수의 숫자를 증가 시킨다고 생각해보자.

class Test:
	a = 0 

#Thread 1: 
Test.a += 1

#Thread 2:
Test.a += 1

print(Test.a)

매우 추상(?)적으로 작성해봤으나 각 스레드에서 a의 값을 1씩 증가 시킨다고 했을 때 결과 값은 2일 것이다. 하지만 실제로 그렇지 않을 수 있다.

위의 코드는 아래와 같이 동작한다.

value = getattr(Test, 'a')
result = value + 1
setattr(Test, 'a', result)

그리고 최악의 경우 컨텍스트 스위칭은 다음과 같이 동작할 수 있다.

value = getattr(Test, 'a') # Thread 1
# 컨텍스트 스위칭
value2 = getattr(Test, 'a') # Thread 2
result = value2 + 1 # Thread 2
setattr(Test, 'a', result) # Thread 2
# 컨텍스트 스위칭
result = value + 1 # Thread 1
setattr(Test, 'a', result) # Thread 1

위와 같이 실행 될 경우 Test의 a 변수의 값은 1이다.

이런 결과는 개발자에게 참으로 당혹스러운 일이다. 다행히도 이 문제는 멀티 스레드에서 각 작업별로 Lock으로 컨텍스트를 관리하여 일관성을 보장할 수 있다. Thread Lock에 대한 내용은 이 글에선 설명하지 않는다.. 각자 구글링을 하도록 하자.

GIL은 우리에게 많은 도움을 주긴하나 완벽하게 안정적인 실행 흐름을 보장해주지 않는다. 그렇기에 항상 멀티 스레드 환경에서 작업할 땐 GIL만을 믿고 작업하기 보단 보수적으로 코드를 작성하는 것이 좋아 보인다.

그럼에도 불구하고 멀티스레드

위의 내용만을 봤을 때는 굳이 여러 리스크나 개발 비용을 감수하면서 까지 멀티스레드를 사용해나 싶지만 동시성 작업에서만큼은 멀티 프로세스보다 효율적인 작업이 가능하기 때문에 상황에 따라 충분히 고려할만한 옵션이다.

기본적으로 GIL은 I/O bound 작업으로 인한 대기시간이 걸릴 경우 컨텍스트 스위칭 하기 때문에 I/O 작업에 대해서 만큼은 싱글 스레드로 직렬 작업 하는 것에 비해 일반적으로 좋은 성능을 보여준다.

마지막으로 요리사 예를 한 번더 들어보자면 요리사 혼자 작업하더라도 면을 끓는 물에 넣고 굳이 멀뚱멀뚱 면이 익을 때까지 보고 있는 것이 아닌 면을 꺼내기 전까지 볶음밥을 볶는 작업을 하는 것이다.

결론

python의 멀티스레드는 GIL 때문에 병렬 작업을 할 수 없으나 I/O bound 작업일 경우 동시 처리를 통해 싱글 스레드 이상의 성능을 기대할 수 있다.
(CPU bound 작업의 경우 멀티 프로세스를 고려해보자)

하지만 멀티 스레드는 위의 GIL의 한계점을 통해 알 수 있듯이 여러 문제점을 야기할 수 있고 컨텍스트 스위칭으로 인한 비용이 동시 처리로 인해 얻는 성능 향상 보다 적을 경우에 유효함으로 확실한 성능 측정 및 충분한 검토 후에 사용해야 한다.

profile
식욕, 수면욕, 성욕 만땅

0개의 댓글