>  기사  >  백엔드 개발  >  Python의 GIL이란 무엇입니까?

Python의 GIL이란 무엇입니까?

王林
王林앞으로
2023-05-14 14:40:061622검색

GIL이 필요한 이유

GIL은 본질적으로 잠금입니다. 운영 체제를 공부한 학생들은 동시 액세스로 인한 데이터 불일치를 방지하기 위해 잠금이 도입된다는 것을 알고 있습니다. CPython에는 메모리 관리의 usable_arenas 및 Usedpools와 같이 함수 외부에 정의된 전역 변수가 많이 있습니다. 여러 스레드가 동시에 메모리에 적용되는 경우 이러한 변수가 동시에 수정되어 데이터 혼란을 일으킬 수 있습니다. 또한 Python의 가비지 수집 메커니즘은 참조 계산을 기반으로 합니다. 모든 개체에는 현재 개체를 참조하는 변수 수를 나타내는 ob_refcnt 필드가 있습니다. 이 함수는 참조 횟수를 줄입니다. 마찬가지로, 여러 스레드가 동일한 개체의 참조 횟수를 동시에 수정하는 경우 ob_refcnt가 실제 값과 다를 수 있으며, 이로 인해 사용되지 않는 개체는 재활용되지 않을 수 있습니다. 심각하게도 참조된 객체로 인해 Python 인터프리터가 충돌할 수 있습니다.

GIL 구현

CPython에서 GIL의 정의는 다음과 같습니다

struct _gil_runtime_state {
    unsigned long interval; // 请求 GIL 的线程在 interval 毫秒后还没成功,就会向持有 GIL 的线程发出释放信号
    _Py_atomic_address last_holder; // GIL 上一次的持有线程,强制切换线程时会用到
    _Py_atomic_int locked; // GIL 是否被某个线程持有
    unsigned long switch_number; // GIL 的持有线程切换了多少次
    // 条件变量和互斥锁,一般都是成对出现
    PyCOND_T cond;
    PyMUTEX_T mutex;
    // 条件变量,用于强制切换线程
    PyCOND_T switch_cond;
    PyMUTEX_T switch_mutex;
};

가장 중요한 것은 GIL이 현재 보유되어 있는지 여부를 나타내는 뮤텍스로 보호되는 잠긴 필드입니다. 다른 필드는 GIL을 최적화하는 데 사용됩니다. 스레드가 GIL을 적용할 때는 take_gil() 메서드를 호출하고, GIL을 해제할 때는 drop_gil() 메서드를 호출합니다. 기아를 피하기 위해 스레드가 간격 밀리초(기본값은 5밀리초) 동안 대기하고 GIL을 적용하지 않은 경우 GIL을 보유하는 스레드에 적극적으로 신호를 보내고 GIL 보유자는 적절한 시간에 신호를 확인합니다. . , 다른 스레드가 적용 중인 것으로 확인되면 GIL이 강제로 해제됩니다. 여기에 언급된 적절한 타이밍은 버전마다 다릅니다. 초기에는 100 명령어마다 확인했습니다. Python 3.10.4에서는 조건문 끝, 루프 문의 각 루프 본문 끝에서 확인했습니다. , 그리고 함수 호출이 끝나면 시간이 되면 확인됩니다.

GIL에 적용되는 take_gil() 함수는 다음과 같이 단순화됩니다

static void take_gil(PyThreadState *tstate)
{
    ...
    // 申请互斥锁
    MUTEX_LOCK(gil->mutex);
    // 如果 GIL 空闲就直接获取
    if (!_Py_atomic_load_relaxed(&gil->locked)) {
        goto _ready;
    }
    // 尝试等待
    while (_Py_atomic_load_relaxed(&gil->locked)) {
        unsigned long saved_switchnum = gil->switch_number;
        unsigned long interval = (gil->interval >= 1 ? gil->interval : 1);
        int timed_out = 0;
        COND_TIMED_WAIT(gil->cond, gil->mutex, interval, timed_out);
        if (timed_out &&  _Py_atomic_load_relaxed(&gil->locked) && gil->switch_number == saved_switchnum) {
            SET_GIL_DROP_REQUEST(interp);
        }
    }
_ready:
    MUTEX_LOCK(gil->switch_mutex);
    _Py_atomic_store_relaxed(&gil->locked, 1);
    _Py_ANNOTATE_RWLOCK_ACQUIRED(&gil->locked, /*is_write=*/1);

    if (tstate != (PyThreadState*)_Py_atomic_load_relaxed(&gil->last_holder)) {
        _Py_atomic_store_relaxed(&gil->last_holder, (uintptr_t)tstate);
        ++gil->switch_number;
    }
    // 唤醒强制切换的线程主动等待的条件变量
    COND_SIGNAL(gil->switch_cond);
    MUTEX_UNLOCK(gil->switch_mutex);
    if (_Py_atomic_load_relaxed(&ceval2->gil_drop_request)) {
        RESET_GIL_DROP_REQUEST(interp);
    }
    else {
        COMPUTE_EVAL_BREAKER(interp, ceval, ceval2);
    }
    ...
    // 释放互斥锁
    MUTEX_UNLOCK(gil->mutex);
}

원자성을 보장하기 위해 전체 함수 본문은 시작과 끝에서 각각 뮤텍스 잠금 gil->뮤텍스를 적용하고 해제해야 합니다. 현재 GIL이 유휴 상태이면 GIL을 직접 가져옵니다. 유휴 상태가 아니면 조건 변수 gil->cond 간격 밀리초(1밀리초 이상) 동안 대기하고 해당 기간 동안 GIL 전환이 발생하지 않습니다. , 강제 전환을 요청하도록 gil_drop_request를 설정합니다. GIL은 스레드를 보유하고 그렇지 않으면 계속 대기합니다. GIL을 성공적으로 획득한 후에는 gil->locked, gil->last_holder 및 gil->switch_number 값을 업데이트해야 하고 조건 변수 gil->switch_cond를 깨워야 하며 뮤텍스 잠금이 해제되어야 합니다. gil->mutex를 해제해야 합니다.

GIL을 해제하는 함수 drop_gil()은 다음과 같이 단순화됩니다

static void drop_gil(struct _ceval_runtime_state *ceval, struct _ceval_state *ceval2,
         PyThreadState *tstate)
{
    ...
    if (tstate != NULL) {
        _Py_atomic_store_relaxed(&gil->last_holder, (uintptr_t)tstate);
    }
    MUTEX_LOCK(gil->mutex);
    _Py_ANNOTATE_RWLOCK_RELEASED(&gil->locked, /*is_write=*/1);
    // 释放 GIL
    _Py_atomic_store_relaxed(&gil->locked, 0);
    // 唤醒正在等待 GIL 的线程
    COND_SIGNAL(gil->cond);
    MUTEX_UNLOCK(gil->mutex);
    if (_Py_atomic_load_relaxed(&ceval2->gil_drop_request) && tstate != NULL) {
        MUTEX_LOCK(gil->switch_mutex);
        // 强制等待一次线程切换才被唤醒,避免饥饿
        if (((PyThreadState*)_Py_atomic_load_relaxed(&gil->last_holder)) == tstate)
        {
            assert(is_tstate_valid(tstate));
            RESET_GIL_DROP_REQUEST(tstate->interp);
            COND_WAIT(gil->switch_cond, gil->switch_mutex);
        }
        MUTEX_UNLOCK(gil->switch_mutex);
    }
}

먼저 gil->mutex 보호 하에 GIL을 해제한 다음, GIL을 기다리고 있는 다른 스레드를 깨웁니다. 다중 CPU 환경에서 현재 스레드는 GIL을 해제한 후 GIL을 다시 획득할 확률이 더 높습니다. 다른 스레드의 고갈을 방지하려면 현재 스레드가 조건 변수 gil->switch_cond를 기다려야 합니다. . 다른 스레드가 있을 때만 GIL을 얻을 수 있습니다. 그런 다음에만 현재 스레드가 깨어납니다.

몇 가지 참고 사항

GIL 최적화

GIL로 제한된 코드는 병렬로 실행될 수 없으므로 전체 성능이 저하됩니다. 성능 손실을 최소화하기 위해 Python은 객체 액세스를 포함하지 않는 IO 작업이나 집약적인 CPU 계산을 적극적으로 수행합니다. GIL을 해제하면

  • 파일 읽기 및 쓰기

  • 네트워크 액세스

  • 암호화된 데이터/압축된 데이터

엄밀히 말하면 단일 프로세스의 경우 GIL의 세분성이 줄어듭니다. , more 두 개의 Python 스레드가 동시에 실행될 수 있습니다. 예를 들어 한 스레드는 정상적으로 실행되고 다른 스레드는 데이터를 압축하고 있습니다.

사용자 데이터의 일관성은 GIL에 의존할 수 없습니다.

GIL은 Python 인터프리터의 내부 변수의 일관성을 유지하기 위해 생성된 잠금입니다. 사용자 데이터의 일관성은 GIL에 책임이 없습니다. GIL은 어느 정도 사용자 데이터의 일관성을 보장하지만, 예를 들어 Python 3.10.4에서 점프 및 함수 호출을 포함하지 않는 명령은 GIL의 제약 조건에 따라 원자적으로 실행되지만 비즈니스 로직에서는 데이터의 일관성이 유지됩니다. 이를 보장하려면 사용자가 직접 잠가야 합니다.

다음 코드는 두 개의 스레드를 사용하여 사용자의 조각 수집을 시뮬레이션하여 보상을 얻습니다.

from threading import Thread

def main():
    stat = {"piece_count": 0, "reward_count": 0}
    t1 = Thread(target=process_piece, args=(stat,))
    t2 = Thread(target=process_piece, args=(stat,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(stat)

def process_piece(stat):
    for i in range(10000000):
        if stat["piece_count"] % 10 == 0:
            reward = True
        else:
            reward = False
        if reward:
            stat["reward_count"] += 1
        stat["piece_count"] += 1

if __name__ == "__main__":
    main()

사용자가 10개의 조각을 수집할 때마다 보상을 받을 수 있다고 가정하고 각 스레드는 10000000개의 조각을 수집하고 9999999개의 보상을 받아야 합니다(마지막 시간). 보상은 없었음) 총 20,000,000개의 조각을 모아야 하고 1,999,998개의 보상을 모아야 하는데, 내 컴퓨터에서 처음 실행한 결과는 다음과 같습니다

{'piece_count': 20000000, 'reward_count': 1999987}

총 조각 수는 예상대로인데, 보상 개수는 12개 더 적습니다. Python 3.10.4에서는 stat["piece_count"] += 1이 GIL 제약 조건에 따라 원자적으로 수행되므로 조각 수가 정확합니다. 실행 스레드는 각 루프의 끝에서 전환될 수 있으므로 스레드 t1이 특정 루프의 끝에서 Piece_count를 100으로 증가시킬 수 있지만 다음 루프가 모듈로 10을 판단하기 시작하기 전에 Python 인터프리터는 스레드로 전환합니다. t2는 실행을 위한 것이며 t2는 Piece_count를 증가시킵니다. 101에 도달하면 보상을 놓치게 됩니다.

첨부파일: GIL의 영향을 받지 않는 방법

그렇게 말했지만 해결책을 이야기하지 않으면 그냥 인기 있는 과학 포스팅일 뿐 아무 소용이 없습니다. GIL이 너무 나빠요. 해결 방법이 있나요? 어떤 솔루션이 있는지 살펴보겠습니다.

스레드를 멀티프로세스로 대체

멀티프로세스 라이브러리의 등장은 주로 GIL로 인한 스레드 라이브러리의 비효율성을 보완하기 위한 것입니다. 마이그레이션을 용이하게 하기 위해 스레드에서 제공하는 인터페이스 세트를 완전히 복제합니다. 유일한 차이점은 다중 스레드 대신 다중 프로세스를 사용한다는 것입니다. 각 프로세스에는 독립적인 GIL이 있으므로 프로세스 간에 GIL 경합이 발생하지 않습니다.

물론 멀티프로세스가 만병통치약은 아닙니다. 이 기능을 도입하면 프로그램의 시간 스레드 간 데이터 통신 및 동기화가 어려워집니다. 카운터를 예로 들어보겠습니다. 여러 스레드가 동일한 변수를 누적하도록 하려면 스레드에 대해 전역 변수를 선언하고 thread.Lock 컨텍스트로 세 줄을 래핑합니다. 다중 프로세스에서는 프로세스가 서로의 데이터를 볼 수 없기 때문에 메인 스레드에서 큐를 선언하고 넣거나 가져오거나 공유 메모리를 사용할 수만 있습니다. 이러한 추가 구현 비용은 이미 매우 고통스러운 멀티 스레드 프로그램 코딩을 더욱 고통스럽게 만듭니다. 구체적인 어려움은 무엇입니까? 관심 있는 독자는 이 기사를 더 자세히 읽어볼 수 있습니다.

다른 파서 사용

앞서 언급했듯이 GIL은 CPython의 제품일 뿐이므로 다른 파서가 더 낫습니까? 예, JPython 및 IronPython과 같은 파서는 구현 언어의 특성으로 인해 GIL의 도움이 필요하지 않습니다. 그러나 파서 구현을 위해 Java/C#을 사용함으로써 그들은 커뮤니티 C 언어 모듈의 많은 유용한 기능을 활용할 기회도 잃었습니다. 따라서 이러한 파서는 항상 상대적으로 틈새 시장이었습니다. 결국, 모든 사람은 기능과 성능보다 전자를 선택하게 될 것이며, 초기 단계에서는 완료된 것이 완벽한 것보다 낫습니다.

그럼 절망적인가요?

물론 Python 커뮤니티에서도 GIL을 지속적으로 개선하기 위해 열심히 노력하고 있으며 심지어 GIL을 제거하려고 시도하고 있습니다. 그리고 각 마이너 버전마다 많은 개선이 이루어졌습니다. 관심 있는 독자는 이 슬라이드를 자세히 읽어보세요

또 다른 개선 사항 GIL 재작업

– 전환 세분성을 opcode 기반 계산에서 시간 분할 기반 계산으로 변경합니다.

– 최근 GIL 잠금을 해제한 스레드가 즉시 실행되지 않도록 하세요. 다시 차단됨 Scheduling

– 스레드 우선순위 기능 추가(우선순위가 높은 스레드는 다른 스레드가 자신이 보유하고 있는 GIL 잠금을 해제하도록 강제할 수 있음)

위 내용은 Python의 GIL이란 무엇입니까?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 yisu.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제