Qu'est-ce que le «stockage local des threads» en Python et pourquoi en ai-je besoin?

100

En Python en particulier, comment les variables sont-elles partagées entre les threads?

Bien que j'aie utilisé threading.Threadavant, je n'ai jamais vraiment compris ou vu des exemples de partage des variables. Sont-ils partagés entre le fil principal et les enfants ou seulement entre les enfants? Quand aurais-je besoin d'utiliser le stockage local des threads pour éviter ce partage?

J'ai vu de nombreux avertissements sur la synchronisation de l'accès aux données partagées entre les threads à l'aide de verrous, mais je n'ai pas encore vu un très bon exemple du problème.

Merci d'avance!

Mike
la source
2
Le titre ne correspond pas à la question. La question est à voir avec le partage de variables entre les threads, le titre implique qu'il s'agit spécifiquement du stockage local des
threads
2
@Casebash: au son de cette question, Mike a lu que TLS est nécessaire pour éviter les problèmes causés par les données partagées, mais il ne savait pas avec quelles données étaient partagées par défaut, avec quoi elles étaient partagées et comment elles étaient partagées. J'ai ajusté le titre pour mieux correspondre à la question.
Shog9 du

Réponses:

83

En Python, tout est partagé, à l'exception des variables locales de fonction (car chaque appel de fonction obtient son propre ensemble de paramètres locaux et les threads sont toujours des appels de fonction séparés.) Et même dans ce cas, seules les variables elles-mêmes (les noms qui font référence aux objets) sont locaux à la fonction; les objets eux-mêmes sont toujours globaux et tout peut s'y référer. L' Threadobjet d'un thread particulier n'est pas un objet spécial à cet égard. Si vous stockez l' Threadobjet à un endroit accessible à tous les threads (comme une variable globale), tous les threads peuvent accéder à cet Threadobjet. Si vous souhaitez modifier de manière atomique tout ce à quoi un autre thread a accès, vous devez le protéger avec un verrou. Et tous les threads doivent bien sûr partager ce même verrou, sinon ce ne serait pas très efficace.

Si vous voulez un stockage réel local des threads, c'est là threading.localqu'intervient. Les attributs de threading.localne sont pas partagés entre les threads; chaque thread ne voit que les attributs qu'il y a lui-même placés. Si vous êtes curieux de connaître sa mise en œuvre, la source se trouve dans _threading_local.py dans la bibliothèque standard.

Thomas Wouters
la source
1
Pouvez-vous donner plus de détails sur la phrase suivante s'il vous plaît? "Si vous voulez modifier de manière atomique tout ce que vous n'avez pas simplement créé dans ce même thread, et que vous n'avez pas stocké n'importe où un autre thread peut y accéder, vous devez le protéger par un verrou."
changyuheng
@changyuheng: Voici une explication de ce que sont les actions atomiques: cs.nott.ac.uk/~psznza/G52CON/lecture4.pdf
Tom Busby
1
@TomBusby: S'il n'y a pas d'autres threads qui peuvent y accéder, pourquoi avons-nous besoin de le protéger par un verrou, c'est-à-dire pourquoi avons-nous besoin de rendre le processus atomique?
changyuheng
2
Pouvez-vous donner un exemple rapide de: "les objets eux-mêmes sont toujours globaux, et tout peut y faire référence". Par référence, supposons que vous entendiez lire et ne pas attribuer / ajouter?
variable du
@variable: Je pense qu'il signifie que les valeurs n'ont pas de portée
user1071847
75

Considérez le code suivant:

#/usr/bin/env python

from time import sleep
from random import random
from threading import Thread, local

data = local()

def bar():
    print("I'm called from", data.v)

def foo():
    bar()

class T(Thread):
    def run(self):
        sleep(random())
        data.v = self.getName()   # Thread-1 and Thread-2 accordingly
        sleep(1)
        foo()
>> T (). Start (); T (). Début ()
Je suis appelé depuis Thread-2
Je suis appelé depuis Thread-1 

Ici, threading.local () est utilisé comme un moyen rapide et sale de passer des données de run () à bar () sans changer l'interface de foo ().

Notez que l'utilisation de variables globales ne fera pas l'affaire:

#/usr/bin/env python

from time import sleep
from random import random
from threading import Thread

def bar():
    global v
    print("I'm called from", v)

def foo():
    bar()

class T(Thread):
    def run(self):
        global v
        sleep(random())
        v = self.getName()   # Thread-1 and Thread-2 accordingly
        sleep(1)
        foo()
>> T (). Start (); T (). Début ()
Je suis appelé depuis Thread-2
Je suis appelé depuis Thread-2 

En attendant, si vous pouviez vous permettre de passer ces données comme argument de foo (), ce serait un moyen plus élégant et bien conçu:

from threading import Thread

def bar(v):
    print("I'm called from", v)

def foo(v):
    bar(v)

class T(Thread):
    def run(self):
        foo(self.getName())

Mais ce n'est pas toujours possible lors de l'utilisation de code tiers ou mal conçu.

ahatchkins
la source
18

Vous pouvez créer un stockage local de thread à l'aide de threading.local().

>>> tls = threading.local()
>>> tls.x = 4 
>>> tls.x
4

Les données stockées dans le tls seront uniques à chaque thread, ce qui aidera à garantir qu'aucun partage involontaire ne se produira.

Aaron Maenpaa
la source
2

Comme dans tous les autres langages, chaque thread en Python a accès aux mêmes variables. Il n'y a pas de distinction entre le «thread principal» et les threads enfants.

Une différence avec Python est que le Global Interpreter Lock signifie qu'un seul thread peut exécuter du code Python à la fois. Cependant, cela n'aide pas beaucoup pour la synchronisation de l'accès, car tous les problèmes de préemption habituels s'appliquent toujours et vous devez utiliser des primitives de thread comme dans d'autres langues. Cela signifie toutefois que vous devez reconsidérer si vous utilisiez des threads pour les performances.

Nick Johnson
la source
0

Je me trompe peut-être ici. Si vous savez le contraire, veuillez expliquer car cela aiderait à expliquer pourquoi il faudrait utiliser thread local ().

Cette déclaration semble éteinte, pas fausse: "Si vous voulez modifier de manière atomique tout ce à quoi un autre thread a accès, vous devez le protéger avec un verrou." Je pense que cette déclaration est -> effectivement <- correcte mais pas tout à fait exacte. Je pensais que le terme "atomique" signifiait que l'interpréteur Python créait un bloc d'octet-code qui ne laissait aucune place pour un signal d'interruption au CPU.

Je pensais que les opérations atomiques sont des morceaux de code octet Python qui ne donnent pas accès aux interruptions. Les instructions Python comme "running = True" sont atomiques. Vous n'avez pas besoin de verrouiller le CPU des interruptions dans ce cas (je crois). La ventilation du code d'octet Python est à l'abri de l'interruption de thread.

Le code Python comme "threads_running [5] = True" n'est pas atomique. Il y a deux morceaux de code d'octet Python ici; un pour dé-référencer la liste () pour un objet et un autre morceau de code d'octet pour attribuer une valeur à un objet, dans ce cas un "endroit" dans une liste. Une interruption peut être déclenchée -> entre <- les deux octets-code -> morceaux <-. C'est là que de mauvaises choses se produisent.

Quel est le rapport entre thread local () et "atomic"? C'est pourquoi la déclaration me semble mal orientée. Sinon, pouvez-vous expliquer?

DevPlayer
la source
1
Cela ressemble à une réponse, mais a été signalé comme problématique, je suppose en raison des questions posées. J'éviterais de demander des éclaircissements dans la réponse. C'est à cela que servent les commentaires.
Dharman le