GIL est essentiellement un verrou. Les étudiants qui ont étudié les systèmes d'exploitation savent que les verrous sont introduits pour éviter les incohérences de données causées par un accès simultané. Il existe de nombreuses variables globales définies en dehors des fonctions dans CPython, telles que usable_arenas et usedpools dans la gestion de la mémoire. Si plusieurs threads demandent de la mémoire en même temps, ces variables peuvent être modifiées en même temps, provoquant une confusion des données. De plus, le mécanisme de récupération de place de Python est basé sur le comptage de références. Tous les objets ont un champ ob_refcnt qui indique le nombre de variables faisant actuellement référence à l'objet actuel. Les opérations telles que l'affectation de variables et le passage de paramètres augmenteront le nombre de références. la fonction réduira le nombre de références. De même, si plusieurs threads modifient le nombre de références d'un même objet en même temps, il est possible que ob_refcnt soit différent de la valeur réelle, ce qui peut provoquer des fuites de mémoire. Les objets qui ne seront pas utilisés ne seront pas recyclés, et plus encore. sérieusement, ils peuvent être recyclés. L'objet référencé a provoqué le crash de l'interpréteur Python.
La définition de GIL dans CPython est la suivante
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; };
La chose la plus essentielle est le champ verrouillé protégé par mutex, qui indique si GIL est actuellement détenu. D'autres champs sont utilisés pour optimiser GIL. Lorsqu'un thread demande GIL, il appelle la méthode take_gil(), et lorsqu'il libère GIL, il appelle la méthode drop_gil(). Afin d'éviter la famine, lorsqu'un thread attend un intervalle de quelques millisecondes (la valeur par défaut est de 5 millisecondes) et n'a pas demandé de GIL, il enverra activement un signal au thread détenant GIL, et le détenteur de GIL vérifiera le signal au moment approprié. , s'il s'avère que d'autres threads s'appliquent, le GIL sera libéré de force. Le timing approprié mentionné ici est différent selon les versions. Au début, il était vérifié toutes les 100 instructions. Dans Python 3.10.4, il était vérifié à la fin de l'instruction conditionnelle, à la fin de chaque corps de boucle de l'instruction de boucle. , et la fin de l'appel de fonction. Elle sera vérifiée le moment venu.
La fonction take_gil() qui s'applique à GIL est simplifiée comme suit
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); }
Afin de garantir l'atomicité, l'ensemble du corps de la fonction doit demander et libérer le verrouillage mutex gil->mutex au début et à la fin respectivement. Si le GIL actuel est inactif, récupérez le GIL directement. S'il n'est pas inactif, attendez la variable de condition gil->cond interval millisecondes (pas moins de 1 milliseconde) s'il expire et qu'aucune commutation de GIL ne se produit pendant la période. , définissez gil_drop_request pour demander une commutation forcée. Le GIL conserve le thread, sinon il continue d'attendre. Une fois le GIL obtenu avec succès, les valeurs de gil->locked, gil->last_holder et gil->switch_number doivent être mises à jour, la variable de condition gil->switch_cond doit être réveillée et le verrouillage mutex gil->mutex doit être libéré.
La fonction drop_gil() qui libère GIL est simplifiée comme suit
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); } }
Libère d'abord le GIL sous la protection de gil->mutex, puis réveille les autres threads qui attendent le GIL. Dans un environnement multi-CPU, le thread actuel a une probabilité plus élevée de réacquérir le GIL après avoir libéré le GIL. Afin d'éviter de priver les autres threads de faim, le thread actuel doit être forcé d'attendre la variable de condition gil->switch_cond. Il ne peut obtenir le GIL que lorsque d'autres threads. Ce n'est qu'alors que le thread actuel sera réveillé.
Le code contraint par GIL ne peut pas être exécuté en parallèle, ce qui réduit les performances globales, afin de minimiser la perte de performances, Python effectuera activement des opérations d'E/S ou des calculs CPU intensifs qui n'impliquent pas d'accès aux objets. Libérer le GIL réduit la granularité du GIL, comme
lecture et écriture de fichiers
accès au réseau
données cryptées/données compressées
Donc à proprement parler, dans le cas d'un seul processus , plus Deux threads Python peuvent être exécutés en même temps. Par exemple, un thread s'exécute normalement et un autre thread compresse les données.
GIL est un verrou généré pour maintenir la cohérence des variables internes de l'interpréteur Python. La cohérence des données utilisateur n'est pas responsable de GIL. Bien que GIL garantisse également dans une certaine mesure la cohérence des données utilisateur, par exemple, les instructions qui n'impliquent pas de sauts et d'appels de fonction dans Python 3.10.4 seront exécutées de manière atomique sous les contraintes de GIL, mais la cohérence des données dans la logique métier. l'utilisateur doit le verrouiller lui-même pour s'en assurer.
Le code suivant utilise deux threads pour simuler la collection de fragments de l'utilisateur afin de gagner des récompenses
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()
Supposons que l'utilisateur puisse obtenir une récompense à chaque fois qu'il collecte 10 fragments. Chaque fil a collecté 1 000 000 fragments et devrait obtenir 9999999 récompenses (la dernière fois. aucune récompense n'a été calculée), un total de 20 000 000 de fragments devraient être collectés et 1 999 998 récompenses devraient être collectées. Cependant, les résultats de la première exécution sur mon ordinateur sont les suivants
{'piece_count': 20000000, 'reward_count': 1999987}
Le nombre total de fragments est comme prévu, mais le le nombre de récompenses est de 12 de moins. Le nombre de pièces est correct car dans Python 3.10.4, stat["piece_count"] += 1 est effectué de manière atomique sous les contraintes GIL. Puisque le thread d'exécution peut être changé à la fin de chaque boucle, il est possible que le thread t1 augmente piece_count à 100 à la fin d'une certaine boucle, mais avant que la boucle suivante ne commence à juger modulo 10, l'interpréteur Python passe au thread t2 pour l'exécution, et t2 augmentera piece_count Si vous atteignez 101, vous manquerez une récompense.
Pièce jointe : Comment éviter d'être affecté par GIL
Cela dit, si je ne mentionne pas la solution, ce n’est qu’un article de vulgarisation scientifique, mais c’est inutile. GIL est si mauvais, y a-t-il un moyen de contourner ce problème ? Jetons un coup d'œil aux solutions disponibles.
Remplacer Thread par multiprocessus
L'émergence de la bibliothèque multiprocessus vise en grande partie à compenser l'inefficacité de la bibliothèque de threads due à GIL. Il réplique complètement un ensemble d'interfaces fournies par thread pour faciliter la migration. La seule différence est qu'il utilise plusieurs processus au lieu de plusieurs threads. Chaque processus possède son propre GIL indépendant, il n'y aura donc aucun conflit de GIL entre les processus.
Bien sûr, le multiprocessus n'est pas une panacée. Son introduction augmentera la difficulté de la communication des données et de la synchronisation entre les threads temporels du programme. Prenons le compteur comme exemple. Si nous voulons que plusieurs threads accumulent la même variable, pour le thread, déclarez une variable globale et enveloppez trois lignes avec le contexte thread.Lock. En multiprocessus, puisque les processus ne peuvent pas voir les données des autres, ils peuvent uniquement déclarer une file d'attente dans le thread principal, la placer puis l'obtenir, ou utiliser la mémoire partagée. Ce coût supplémentaire de mise en œuvre rend le codage de programmes multithread, déjà très pénible, encore plus pénible. Quelles sont les difficultés spécifiques ? Les lecteurs intéressés peuvent lire davantage cet article
Utiliser d'autres analyseurs
Comme mentionné précédemment, puisque GIL n'est qu'un produit de CPython, les autres analyseurs sont-ils meilleurs ? Oui, les analyseurs comme JPython et IronPython ne nécessitent pas l'aide du GIL en raison de la nature de leurs langages d'implémentation. Cependant, en utilisant Java/C# pour l'implémentation de l'analyseur, ils ont également perdu l'opportunité de profiter des nombreuses fonctionnalités utiles des modules de langage C de la communauté. Ces analyseurs ont donc toujours été relativement spécialisés. Après tout, tout le monde choisira le premier plutôt que la fonction et les performances au début. Mieux vaut faire que parfait.
Alors c’est désespéré ?
Bien sûr, la communauté Python travaille également très dur pour améliorer continuellement le GIL, et essaie même de supprimer le GIL. Et il y a eu de nombreuses améliorations dans chaque version mineure. " encore une fois Planification
– Ajout de la fonction de priorité des threads (les threads de haute priorité peuvent forcer d'autres threads à libérer les verrous GIL qu'ils détiennent)
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!