ホームページ  >  記事  >  バックエンド開発  >  PythonのGILとは何ですか

PythonのGILとは何ですか

王林
王林転載
2023-05-14 14:40:061601ブラウズ

GIL が必要な理由

GIL は本質的にロックです。オペレーティング システムを勉強したことがある学生は、ロックが同時アクセスによって引き起こされるデータの不整合を避けるために導入されていることを知っています。 CPython には、メモリ管理における used_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 が現在ロックされているかどうかを示す、mutex によって保護されたロックされたフィールドです。他のフィールドは、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->mutexそれぞれ最初と最後に。現在の 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 の粒度が低下します

  • ネットワークアクセス

  • 暗号化データ/圧縮データ

厳密に言えば、単一プロセスの場合、複数の Python スレッドが同時にアクセスされる可能性があります。たとえば、1 つのスレッドが通常に実行され、別のスレッドがデータを圧縮しています。

ユーザー データの一貫性は GIL に依存できません

GIL は、Python インタプリタの内部変数の一貫性を維持するために生成されるロックであり、ユーザー データの一貫性は GIL には関与しません。 GIL はユーザー データの一貫性もある程度保証しますが、たとえば Python 3.10.4 ではジャンプや関数呼び出しを含まない命令は GIL の制約の下でアトミックに実行されますが、ビジネス ロジックにおけるデータの一貫性は保証されません。確実にロックするには、ユーザーが自分でロックする必要があります。

次のコードは、2 つのスレッドを使用してユーザーのフラグメントのコレクションと賞の受賞をシミュレートします

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 個のフラグメントを収集するたびに報酬を得ることができ、各スレッドが 10,000,000 個のフラグメントを収集したと仮定します。フラグメント、9999999 個の報酬が取得されたはずです (前回は計算されていません)、合計 20000000 個のフラグメントが収集され、1999998 個の報酬が取得されましたが、私のコンピューターでの最初の実行の結果は次のとおりでした

{'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 コンテキストを 3 行で囲みます。マルチプロセスでは、プロセスは互いのデータを参照できないため、メインスレッドでキューを宣言し、put してから get するか、共有メモリを使用することしかできません。この追加の実装コストにより、すでに非常に困難なマルチスレッド プログラムのコーディングがさらに困難になります。具体的な問題点はどこですか? 興味のある方はこの記事をさらに読んでください。

他のパーサーを使用する

前述したように、GIL は CPython の製品にすぎないため、他のパーサーの方が優れていますか?はい、JPython や IronPython などのパーサーは、実装言語の性質上、GIL の助けを必要としません。ただし、パーサーの実装に Java/C# を使用することにより、コミュニティの C 言語モジュールの多くの便利な機能を活用する機会も失いました。したがって、これらのパーサーは常に比較的ニッチなものでした。結局のところ、初期段階では誰もが機能やパフォーマンスよりも前者を選択することになります。

つまり、それは絶望的ですか?

もちろん、Python コミュニティも GIL を継続的に改善するために熱心に取り組んでおり、GIL を削除しようとさえしています。そして、各マイナーバージョンでは多くの改善が行われています。興味のある方はこのスライドをさらに読んでください。

#別の改善 GIL の再加工

– スイッチング粒度をオペコード カウントに基づくものからタイム スライス カウントに基づくものに変更します

&ndash ; 防止最近 GIL ロックを解放したスレッドがすぐに再度スケジュールされるようにします

– スレッド優先度機能を追加しました (優先度の高いスレッドは、他のスレッドが保持している GIL ロックを強制的に解放できます)

以上がPythonのGILとは何ですかの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はyisu.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。