Programming/PYTHON

GIL(Global interpreter Lock)

우드의개발개발 2023. 2. 4. 14:43

GIL은 python 을 구현한 CPython에서 GIL를 제공함으로써 GC에서 reference count가 0이 되지 않는 상황을 방지하기 위해 도입되었다. That only one thread can execute Python bytecodes at a time. 문장에서 보면 알 수 있듯이 여러개의 쓰레드가 있다 하더라도 오직 하나의 쓰레드만 파이썬 코드를 실행시킬 수 있다. 앞서 말했지만 하나의 쓰레드만 파이썬 객체에 접근하게 함으로써 메모리를 잘 관리하는데 그 탄생 배경이 있다.

 

하지만 공유 자원과 관련해 싱글 스레드만을 허용했다고 해서 동기화가 잘 적용되는 것은 아니다. 동기화를 적용시키게끔 코드로 녹여내야 한다. 동기화가 잘 적용되지 않는 이유는 GIL이 하나의 thread만을 허용하면서 thread 단위로 처리하는 것이 아니고 thread가 실행하는 코드를 바이트코드로 변환하여 바이트 코드 단위로 처리하기 때문에 공유자원에 대한 동기화가 잘 적용되지 않는 것이다.

 

따라서 GIL로 인해 멀티 쓰레드 환경이라 해도 코드에서 동기화가 적용되는 것이 아니기 때문에 동기화가 이루어지게끔 코드를 작성해야 thread-safe한 환경을 조성할 수 있다.

예를 들어 하나의 쓰레드 별로 1,000,000만큼의 숫자를 증가시킨다고 해서 두개의 쓰레드를 병렬적으로 실행시켰을 때 공유자원의 결과가 4,000,000이 된다는 것을 의미하지는 않는다.

 

import time
import threading

counter = 0
def increment_counter():
    global counter
    for i in range(1000000):
        counter += 1

t1 = threading.Thread(target=increment_counter)
t2 = threading.Thread(target=increment_counter)
t3 = threading.Thread(target=increment_counter)
t4 = threading.Thread(target=increment_counter)

t1.start()
t2.start()
t3.start()
t4.start()

t1.join()
t2.join()
t3.join()
t4.join()
end = time.time()

print(counter)

위 코드를 실행 했을 때 counter가 4,000,000이 되는 것을 의미하지 않는다.

 

import threading

counter = 0
lock = threading.Lock()
def increment_counter():
    global counter
    for i in range(1000000):
        with lock:
            counter += 1

t1 = threading.Thread(target=increment_counter)
t2 = threading.Thread(target=increment_counter)
t3 = threading.Thread(target=increment_counter)
t4 = threading.Thread(target=increment_counter)

t1.start()
t2.start()
t3.start()
t4.start()

t1.join()
t2.join()
t3.join()
t4.join()

print(counter)

위 코드처럼 락을 걸음으로 써 GIL을 적용시켜 동기화가 보장되는 코드를 작성해야 한다.

 

thread-safe 한 코드 작성 또는 시스템과 관련하여 아래의 지침서를 따르면 좋다.

  1. Atomic Operations: Thread-safe code ensures that certain operations are atomic, meaning they are executed as a single, indivisible unit. Atomic operations guarantee that no other thread can interrupt or observe an intermediate state during the execution of the operation. For example, incrementing a counter or updating a shared variable atomically ensures that the operation is thread-safe.
  2. Synchronization Mechanisms: Thread-safe code employs synchronization mechanisms, such as locks, semaphores, condition variables, or other constructs, to control access to shared resources. These mechanisms provide mutual exclusion or coordination among threads, ensuring that only one thread can access the shared resource at a given time. Synchronization prevents race conditions and maintains consistency of data.
  3. Data Integrity: Thread-safe code ensures that shared data remains consistent and intact when accessed concurrently by multiple threads. It prevents issues like data corruption, data loss, or inconsistent states that can occur due to concurrent modifications. Thread-safe code handles critical sections and shared data in a way that preserves data integrity.
  4. Concurrent Access: Thread-safe code allows multiple threads to access shared resources concurrently without interfering with each other's operations. It enables efficient parallelism and maximizes the utilization of CPU cores. Thread-safe systems are designed to handle concurrency gracefully, ensuring that thread interactions do not lead to deadlocks, livelocks, or other synchronization-related problems.
  5. Scalability: Thread-safe code is designed to scale well as the number of threads increases. It can efficiently handle a large number of concurrent threads without sacrificing correctness or performance. Thread-safe systems take into account factors like contention, lock granularity, and overall system design to optimize scalability.

 

또한 CPU bound 작업의 경우 컨텍스트 스위칭 비용까지 생겨 2개 이상의 쓰레드로 실행했다고 해서 높은 퍼포먼스를 보장하지도 않는다.

 

I/O bound 작업이 많은 프로세스 내에서 멀티쓰레딩이 유용하며 탄생의 배경에 메모리 관리가 주된 목적이었다는 것을 기억해야 한다.

 

참고글

'Programming > PYTHON' 카테고리의 다른 글

OOP 스럽게 Python 작성하기  (0) 2023.05.31
map Iterable Iterator  (0) 2023.02.14