일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 29 | 30 | 31 |
- codewars
- ProxyServer
- vscode
- 조엘온소프트웨어
- 코로나백신
- maxlinelength
- pep8
- Golang
- Lint
- 글쓰기가필요하지않은인생은없다
- organizeImports
- 유닉스의탄생
- codewar
- goalng
- Algorithm
- loadimpact
- 오큘러스퀘스트2
- springboot
- python
- 독후감
- opensouce
- flake8
- conf
- GlobalInterprintLock
- printer_helper
- typevar
- restfulapi
- pyenv
- 규칙없음
- httppretty
- Today
- Total
일상적 이야기들.
GIL 에 대해서 이야기를 해보자. 본문
https://wiki.python.org/moin/GlobalInterpreterLock
GIL 은 무엇인가
CPython에서 존재하는 개념으로, Jython, IronPython인 경우에는 GIL 이 존재하지 않습니다.
GIL 은 Global Interprint Lock의 약어입니다.
In CPython, the global interpreter lock, or GIL, is a mutex that protects access to Python objects, preventing multiple threads from executing Python bytecodes at once. This lock is necessary mainly because CPython's memory management is not thread-safe.
CPython에서는 Python 바이트코드를 수행을 여러 스레드에서 한번에 경우, Python Object에 하나의 스레드만을 접근하도록 보호하는 mutex이다.
이러한 Lock이 필요한 이유는, CPython의 메모리 관리가 Thread-safe하지 않기때문이다.
Python 인터프리터는 내부적으로 스레드 안전하지 않으므로, 전역 인터프리터 락을 가지고 있다. GIL 은 한번에 한 스레드만 파이썬 바이트코드를 실행하도록 제한한다. 그렇기 때문에 단일 파이썬 프로세스가 동시에 다중 CPU 코어를 사용할 수 없다.
[전문가를 위한 파이썬 17.2]
The GIL makes sure there is, at any time, only one thread running. Because only one thread can run at a time, it’s impossible to use multiple processors with threads.
GIL 은 어떠한 상황에서도 하나의 스레드만을 수행할 수 있도록 만들며, 그렇기에 멀티 프로세서를 작동할 수 없도록 한다.
만약 멀티프로세서를 사용하고 싶다면, multiprocessing library를 사용해야한다.
GIL 의 작동 원리
GIL 이전의 공유 메모리에 대한 짧은 사전 지식들.
- 운영체제가 생성하는 작업 단위를 process라고 한다. 이 process 안에서 공유되는 메모리를 바탕으로 여러 작업을 또 생성할 수 있는데, 이 때의 작업 단위를 thread라고 한다. 따라서 각 thread 마다 할당된 개인적인 메모리가 있으면서, thread가 속한 process가 가지는 메모리에도 접근할 수 있다.
thread-unsafe의 상태는 위의 도식과 같이 공유 자원에 여러 스레드가 접근을 하여 값을 변경할 수 있는 상태를 의미한다.
이러한 공통 자원을 병행적으로 읽거나 쓰는 동작을 할 때, 공용 데이터에 대한 접근이 어떤 순서에 따라 이루어졌는지에 따라 그 실행 결과가 같지 않고 달라지는 상황을 말한다. 이러한 상태를 경쟁 상태 (race condition) 이라고 말을 하며, 경쟁 상태에서는 3가지의 문제에 직면한다.
- Mutual exclusion
- 공유 자원에 한 프로세스에 의해서 읽거나 쓰여지고 있을 시에, 다른 프로세스가 해당 공유자원의 접근을 하지 못하도록 하면 경쟁 상태를 예방할 수 있다. 이를 상호배제라고 부른다.
- deadlock
- 공유 자원의 종류가 여러개가 되었을 때, 다음과 같은 상황에 직면할 수 있다.Thead1은 자신의 작업을 끝내기 위해서 Resouce2가 필요한 상황이다. 그러나 Resouce2는 Thread2가 선점하였기때문에 접근이 불가능하다. Thread2가 일을 끝내고 Resouce2를 반환하기까지 기다려야하는데, Thread2는 자신의 일을 끝내기 위해서는 Resouce1이 필요한 상황이다.
이렇게 각 프로세스가 끝나기 위해서 필요한 자원들이 다른 프로세스에 선점이 되어 종료할 수 있는 상황이 오게 된다. 교착 상태에 빠진 이 친구들을 어떻게 구제해야할까...
- 공유 자원의 종류가 여러개가 되었을 때, 다음과 같은 상황에 직면할 수 있다.Thead1은 자신의 작업을 끝내기 위해서 Resouce2가 필요한 상황이다. 그러나 Resouce2는 Thread2가 선점하였기때문에 접근이 불가능하다. Thread2가 일을 끝내고 Resouce2를 반환하기까지 기다려야하는데, Thread2는 자신의 일을 끝내기 위해서는 Resouce1이 필요한 상황이다.
- starvation
- 기아 상태는, 교착상태에 기인하여 나타는 현상이다. 교착상태는 Resouce 관점에서 이야기를 하고 있으며 기상태는 Process 관점에서 이야기를 한다. 위의 상황에서 두 Process는 각각 필요한 자원을 선점하고 있는 Process가 끝나기만을 바라고 있으며, 결과적으로는 둘 모두 완료되지 못하여 굶어가는 상태가 된다.
이러한 Race condition을 해결할 수 있는 방안이, 세마포어와 뮤텍스가 존재한다.
뮤텍스는, 공유자원에 하나의 Thread만을 접근할 수 있도록 관리하는 객체를 의미한다. Thread 들은 객체를 접근하기 위해서 mutex를 확인하게 되고 사용 가능한 상태이면, mutex의 상태를 변경하고 공유 자원에 접근을 하게 된다.
그리고 공유자원의 사용이 끝이났다면, 다시 mutex의 상태를 변경한다.
세마포어는, 뮤텍스의 상위호환으로 뮤텍스가 단 하나의 스레드만을 접근할 수 있도록 처리를 하였다면 세마포어는 Count를 가지고 있어서, 허용한 Count만큼의 스레드가 공유자원에 접근할 수 있다.
결국엔 Count가 1인 세마포어를, 뮤텍스라고 부르는 것과 동일하다.
파이썬 Object
GIL의 설명을 보면, Python Object에 하나의 프로세스만 접근 시키기 위해서 필요한 것이라고 적혀있습니다.
그렇다면 Python Object들은 무엇인가요?
파이썬은 모든 것들이 Object로 구성이 되어있습니다. 그렇기에 모든 자원들을 접근하기 위해서는 GIL을 통해서 제어가 됩니다.
이러한 객체들은 Referce Couting에 의하여 메모리 관리가 됩니다. 할당되거나 재 참조되거나 혹은 메모리의 생명주기에 의해서 사라질때마다 Reference Couting을 올리거나 내리면서 말입니다. 그렇기에 이러한 Reference Couting이 공유 자원의 역할이 되는 것이고, 여러 Thread 혹은 Process에 의해서 참조가 된다면 메모리 관리가 안되는 대참사를 일으킬 수 있습니다.
sys.getreferencecount() 함수를 통하여 메모리가 어떻게 참조되고 있는지 확인할 수 있다.
In [1]: import sys
In [2]: sys.getrefcount(test)
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-2-c494f1802eca> in <module>
----> 1 sys.getrefcount(test)
NameError: name 'test' is not defined
In [3]: test = "TestCode"
In [4]: sys.getrefcount(test)
Out[4]: 2
In [5]: del test
In [6]: sys.getrefcount(test)
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-6-c494f1802eca> in <module>
----> 1 sys.getrefcount(test)
NameError: name 'test' is not defined
In [7]: test = "TestCode"
In [8]: sys.getrefcount(test)
Out[8]: 2
In [9]: test2 = test
In [10]: sys.getrefcount(test)
Out[10]: 3
In [11]: sys.getrefcount("TestCode")
Out[11]: 4
In [12]: del test2
In [13]: sys.getrefcount("TestCode")
Out[13]: 3
In [14]: id("TestCode")
Out[14]: 4381097520
In [15]: id(test)
Out[15]: 4381097520
In [16]: id(test2)
간단하게 reference가 어떻게 관리가 되는지 확인하는 코드를 작성해보았다.
test = "TestCode" 문장을 통해서, python은 "TestCode"를 immutable 객체로 메모리에 올린다. ( 4381097520) 주소.
그리고, test라는 변수 객체의 주소를 동일한 주소에 가르키게 된다. 그리고 해당값에 대한 reference Count를 확인하게 되면, "2" 로 확인이 된다.
이때 test는 getreferecnecount에 의해서 한번 더 참조되었기때문에 "1"이 아닌 "2"로 표기가 되는 것이다.
그 이후, test2 의 변수 객체에 test를 넣게 되면, test의 reference count는 하나 증가하게 된다.
정리.
파이썬의 모든 것들은 Object로 이뤄져있으며, Object의 관리는 Reference Counting에 의해서 이뤄지게 된다.
Reference Couting이 잘못 동작하게 될 시에, 메모리 유실이 발생할 수 있으므로 큰 문제가 된다. 그렇기에 Reference Couting에 접근하기 위해서 mutex를 이용하였으며, 이를 GIL로 부르기로 하였다.
그리고 또한, Reference Couting에 대한 요구 사항때문에 파이썬이 하나의 쓰레드가 하나의 Python code를 수행하도록 정리가 된 것이다. Reference Couting을 모두 처리하기에는 어려우므로, 한번에 하나의 Python Code를 수행하게 되면, Re
GIL 이 Python 에게 끼치는 영향
GIL을 통해서 Python이 Thread를 아무리 많이 생성을 하여도 한번에 하나의 작업만을 수행하는 것을 확인을 하였다.
위의 그림과 같이 파이썬에 Thread가 여러개 실행이 될 시에는 Context Switch 비용이 발생하게끔 된다. 이는 쓰레드 사이에 전환이 빈번할 수록 우리가 생각했던 쓰레드 성능에서 멀어질 수 있다는 점을 시사한다.
또한 GIL 에 의해서 한번에 하나의 Python code를 수행하기때문에 성능이 제한적일 수 밖에 없다.
파이썬 코드를 작성할 때 우리는 GIL을 제어할 수 없지만, 내장 함수나 C로 작성된 확장은 시간이 오래 걸리는 작업을 실행할 때 GIL을 해제할 수 있다. 사실 C로 작성된 파이썬 라이브러리는 GIL을 관리하고, 자신의 OS 스레드를 생성해서 가용한 CPU 코드를 모두 사용할 수 있다. 하지만 라이브러리 코드가 상당히 복잡해지므로, 대부분의 라이브러리 제작자는 이런 방식으로 구현하지 않는다.
그런데 블로킹 입출력을 실행하는 모든 표준 라이브러리 함수는 OS에서 결과를 기다리는 동안 GIL을 해제한다. 즉, 입출력 위주의 작업을 실행하는 파이썬 프로그램은 파이썬으로 구현하더라도 스레드를 이용함으로써 이득을 볼 수 있다는 것이다. 파이썬 스레드가 네트워크로부터의 응답을 기다리는 동안, 블로킹된 입출력 함수가 GIL을 해제함으로써 다른 스레드가 실행될 수 있다.
이런 이유로 데이비드 비즐리는 '파이썬 스레드를 아주 능숙하게 게으름을 피운다'고 한다.
Compiler vs Interprint
Compiler이 되면 어떤 것이 튀어나오나?
C/C++ 언어를 컴파일하게 되면, 목적코드 혹은 목적 파일이 나온다.
목적 파일들은 기계어나 혹은 이에 준하는 이진코드로 이루어져있으며, 링커는 여러개의 목적 파일을 묶어 커널과 연결함으로써 실행파일을 만들거나, 혹은 라이브러리를 만들어내는 데에 쓰인다.
목적 파일에서 필수적인 요소는 기계어이다. 임베디드 시스템을 위한 목적 파일들은 기계어 이외는 아무것도 포함하고 있지 않다.
Interprint언어의 수행 순서
인터프린터는 고급언어로 작성된 원시코드 명령어들을 한번에 한 줄 씩 읽어들여서 실행하는 프로그램이다. 고급언어로 작성된 프로그램들을 실행하는 데에는 두가지 방법이 있다. 가장 일반적인 방법은 프로그램을 컴파일 하는 것이고, 다른 하나는 프로그램을 인터프리터에 통과시키는 방법이다.
인터프리터는 고급 명령어들을 중간 형태로 번역한 다음, 그것을 실행한다.
인터프린터 언어의 장점은 기계어 명령어들이 만들어지는 컴파일 단계를 거칠 필요가 없다는데 있다. 컴파일 과정은 만약 원시프로그램의 크기가 크다면 컴파일 시간이 상당히 걸릴 수 있지만, 인터프린터의 장점은 바로 실행이 가능하기때문이다.
Interprint언어는 Machine language로 해석되지 않는가?
import dis
def test():
print("TestCode")
dis.dis(test)
Out:
2 0 LOAD_GLOBAL 0 (print)
2 LOAD_CONST 1 ('Test Code')
4 CALL_FUNCTION 1
6 POP_TOP
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
Java의 동작원리는?
출처 : https://gyoogle.dev/blog/computer-language/Java/컴파일 과정.html
Java는 Javacompile에 의하여, 자바바이트코드(.class)로 컴파일이되며, 이는 JVM이 해석할 수 있는 단계의 코드이다. 바이트코드의 각 명령어는 1바이트 크기의 Opcode와 추가 피연산자로 이루어져있습니다.
.class 파일을 JVM이 읽고 다음과 같은 상태로 동작을 하게 된다.
- 클래스 로더 세부 동작
- 로드 : 클래스 파일을 가져와서 JVM의 메모리에 로드한다.
- 검증: 자바 언어 명세 및 JVM 명세에 명시된 대로 구성되어 있는지 검사한다
- 준비: 클래스가 필요로 하는 메모리를 할당한다. (필드, 메서드, 인터페이스 등)
- 분석: 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경한다.
- 초기화: 클래스 변수들을 적절한 값으로 초기화한다. (static 필드)
- 실행엔진은 JVM 메모리에 올라온 바이트 코드들을 명령어 단위로 하나씩 가져와서 실행한다. 이때, 실행 엔진은 두가지 방식으로 변경한다.
- 인터프리터 : 바이트 코드 명령어를 하나씩 읽어서 해석하고 실행한다. 하나하나의 실행은 빠르나, 전체적인 실행 속도가 느리다는 단점을 가진다.
- JIT 컴파일러 : 인터프린터의 단점을 보완하기 위해 도입된 방식으로 바이트 코드 전체를 컴파일하여 바이너리 코드로 변경하고 이후에는 해당 메서드를 더이상 인터프리팅 하지 않고, 바이너리 코드로 직접 실행하는 ㅂ아식이다. 하나씩 인터프리팅하여 실행하는 것이 아니라 바이트 코드 전체가 컴파일된 바이너리 코드를 실행하는 것이기 때문에 전체적인 실행속도는 인터프리팅 방식보다 빠르다.
Java에는 GIL 이 없나?
출처 : https://velog.io/@litien/GIL-Java에는-없던데
- 없음.
Java는 Mark and Sweep (GC)으로 메모리를 관리를 하기때문에, 루트부터 하나씩 참조를 따라가면서 찾은 객체가 살아있음 Marking 한 후 전체 힙 객체들을 순회하면서 마킹되지 않은 객체들을 제거하는 방식이다.
그렇기에 GIL 개념이 필요없다.
GC 에서는 메모리가 일정수준 이상 찼을 때 위에서 언급한 컬렉팅 작업이 시작이 되며, 때문에 오브젝트의 참조가 변경 될 때마다 Atomic한 연산이 수행될 필요가 없다.
대신 참조를 확인하는 Mark 과정에서 모든 스레드가 일시적으로 중단시켜 gc의 atomic을 보장한다. 이 시기를 Stop the world라 부르는데 이 stop the world를 줄이기 위해서 GC 알고리즘이 발전해나가고 있다.
'프로그래밍 > PYTHON' 카테고리의 다른 글
Python - Sort 알고리즘 (0) | 2023.08.24 |
---|---|
flake8과 black의 조합. (0) | 2021.07.04 |
Vscode Setting 정보 (0) | 2021.01.08 |
Mac - Bigsur 에서 pyenv 문제 (0) | 2021.01.03 |
[TIL] Pytest - mock server 구축 (0) | 2020.09.23 |