web-dev-qa-db-fra.com

Utilisation d'un dictionnaire global avec des threads en Python

L'accès/la modification des valeurs du dictionnaire est-il sûr pour les threads?

J'ai un dictionnaire global foo et plusieurs threads avec des identifiants id1, id2, ..., idn. Est-il correct d'accéder et de modifier les valeurs de foo sans lui allouer un verrou s'il est connu que chaque thread ne fonctionnera qu'avec sa valeur liée à l'ID, par exemple thread avec id1 ne fonctionnera qu'avec foo[id1]?

40
Alex

En supposant que CPython: Oui et non. Il est en fait sûr de récupérer/stocker des valeurs à partir d'un dictionnaire partagé dans le sens où plusieurs requêtes de lecture/écriture simultanées ne corrompront pas le dictionnaire. Cela est dû au verrou interprète global ("GIL") maintenu par l'implémentation. C'est:

Thread A en cours d'exécution:

a = global_dict["foo"]

Thread B en cours d'exécution:

global_dict["bar"] = "hello"

Thread C en cours d'exécution:

global_dict["baz"] = "world"

ne corrompra pas le dictionnaire, même si les trois tentatives d'accès ont lieu en même temps. L'interpréteur les sérialisera d'une manière indéfinie.

Cependant, les résultats de la séquence suivante ne sont pas définis:

Fil A:

if "foo" not in global_dict:
   global_dict["foo"] = 1

Fil B:

global_dict["foo"] = 2

car le test/défini dans le thread A n'est pas atomique (condition de concurrence critique "time-of-check/time-of-use"). Donc, il est généralement préférable que vous verrouillez les choses :

from threading import RLock

lock = RLock()

def thread_A():
    with lock:
        if "foo" not in global_dict:
            global_dict["foo"] = 1

def thread_B():
    with lock:
        global_dict["foo"] = 2
59
Dirk

La meilleure façon, la plus sûre et portable pour que chaque thread fonctionne avec des données indépendantes est:

import threading
tloc = threading.local()

Désormais, chaque thread fonctionne avec un objet tloc totalement indépendant, même s'il s'agit d'un nom global. Le thread peut obtenir et définir des attributs sur tloc, utilisez tloc.__dict__ s'il a spécifiquement besoin d'un dictionnaire, etc.

Le stockage local du thread pour un thread disparaît à la fin du thread; pour que les threads enregistrent leurs résultats finaux, demandez-leur put leurs résultats, avant de se terminer, dans une instance commune de Queue.Queue (qui est intrinsèquement thread-safe). De même, les valeurs initiales des données sur lesquelles un thread doit travailler peuvent être des arguments transmis au démarrage du thread ou être extraits d'un Queue.

D'autres approches mi-cuites, telles que l'espoir que les opérations qui semblent atomiques sont effectivement atomiques, peuvent arriver à fonctionner pour des cas spécifiques dans une version et une version de Python données, mais pourraient facilement être interrompues par des mises à niveau ou des ports. Il n'y a aucune raison réelle de risquer de tels problèmes lorsqu'une architecture appropriée, propre et sûre est si facile à organiser, portable, pratique et rapide.

27
Alex Martelli

Comme j'avais besoin de quelque chose de similaire, j'ai atterri ici. Je résume vos réponses dans ce court extrait:

#!/usr/bin/env python3

import threading

class ThreadSafeDict(dict) :
    def __init__(self, * p_arg, ** n_arg) :
        dict.__init__(self, * p_arg, ** n_arg)
        self._lock = threading.Lock()

    def __enter__(self) :
        self._lock.acquire()
        return self

    def __exit__(self, type, value, traceback) :
        self._lock.release()

if __name__ == '__main__' :

    u = ThreadSafeDict()
    with u as m :
        m[1] = 'foo'
    print(u)

en tant que tel, vous pouvez utiliser la construction with pour maintenir le verrou tout en jouant dans votre dict()

20
yota

GIL s'occupe de cela, si vous utilisez CPython.

verrouillage global de l'interpréteur

Le verrou utilisé par les threads Python pour garantir qu'un seul thread s'exécute dans la machine virtuelle CPython à la fois. Cela simplifie l'implémentation de CPython en garantissant qu'aucun processus ne peut accéder simultanément à la même mémoire. Le verrouillage de l'intégralité de l'interpréteur facilite le multithread pour l'interpréteur, au détriment de la plupart des parallélismes qu'offrent les machines multiprocesseurs. Des efforts ont été faits par le passé pour créer un interprète "libre" ( celui qui verrouille les données partagées à une granularité beaucoup plus fine), mais jusqu'à présent, aucune n'a réussi car les performances ont souffert dans le cas d'un seul processeur commun.

Voir sont-verrous-inutiles-en-code-python-multi-thread-à-cause-du-gil .

6
gimel

Comment ça fonctionne?:

>>> import dis
>>> demo = {}
>>> def set_dict():
...     demo['name'] = 'Jatin Kumar'
...
>>> dis.dis(set_dict)
  2           0 LOAD_CONST               1 ('Jatin Kumar')
              3 LOAD_GLOBAL              0 (demo)
              6 LOAD_CONST               2 ('name')
              9 STORE_SUBSCR
             10 LOAD_CONST               0 (None)
             13 RETURN_VALUE

Chacune des instructions ci-dessus est exécutée avec le verrouillage GIL et l'instruction STORE_SUBSCR ajoute/met à jour la paire clé + valeur dans un dictionnaire. Vous voyez donc que la mise à jour du dictionnaire est atomique et donc thread-safe.

0
Jatin Kumar