Thread lokalen Speicher in Python

73

Wie verwende ich den lokalen Thread-Speicher in Python?

verbunden

Casebash
quelle
1
Ich bin nicht sicher, was Sie fragen - threading.local ist dokumentiert, und Sie haben die folgende Dokumentation mehr oder weniger eingefügt ...
Glenn Maynard
2
@Glenn Ich habe die Dokumentation in eine meiner Antworten eingefügt . Ich zitierte Alex 'Lösung in der anderen. Ich mache diesen Inhalt einfach zugänglicher.
Casebash
Stellen Sie sich vor, Sie kritisieren hilfreiche Freiwillige für die Neuformatierung kritischer Dokumentationen als mobil zugängliche StackOverflow-Antwort, die zuvor nur durch manuelles Eingeben verschleierter Python-Anweisungen in eine interaktive CLI-REPL (z import _threading_local as tl\nhelp(tl). B. ) lesbar war . </yikes>
Cecil Curry

Antworten:

126

Der lokale Thread-Speicher ist beispielsweise nützlich, wenn Sie über einen Thread-Worker-Pool verfügen und jeder Thread Zugriff auf seine eigene Ressource benötigt, z. B. eine Netzwerk- oder Datenbankverbindung. Beachten Sie, dass das threadingModul das reguläre Konzept von Threads verwendet (die Zugriff auf die globalen Prozessdaten haben), diese jedoch aufgrund der globalen Interpretersperre nicht allzu nützlich sind. Das unterschiedliche multiprocessingModul erstellt für jeden einen neuen Unterprozess, sodass jeder globale Thread lokal ist.

Gewindemodul

Hier ist ein einfaches Beispiel:

import threading
from threading import current_thread

threadLocal = threading.local()

def hi():
    initialized = getattr(threadLocal, 'initialized', None)
    if initialized is None:
        print("Nice to meet you", current_thread().name)
        threadLocal.initialized = True
    else:
        print("Welcome back", current_thread().name)

hi(); hi()

Dies wird ausgedruckt:

Nice to meet you MainThread
Welcome back MainThread

Eine wichtige Sache, die leicht übersehen wird: Ein threading.local()Objekt muss nur einmal erstellt werden, nicht einmal pro Thread oder einmal pro Funktionsaufruf. Die globaloder classEbene sind ideale Standorte.

Hier ist der Grund: threading.local()Erstellt tatsächlich jedes Mal eine neue Instanz, wenn sie aufgerufen wird (genau wie bei jedem Factory- oder Klassenaufruf). Wenn Sie also threading.local()mehrmals aufrufen, wird das ursprüngliche Objekt ständig überschrieben, was aller Wahrscheinlichkeit nach nicht das ist, was Sie wollen. Wenn ein Thread auf eine vorhandene threadLocalVariable zugreift (oder wie auch immer sie genannt wird), erhält er eine eigene private Ansicht dieser Variablen.

Dies funktioniert nicht wie beabsichtigt:

import threading
from threading import current_thread

def wont_work():
    threadLocal = threading.local() #oops, this creates a new dict each time!
    initialized = getattr(threadLocal, 'initialized', None)
    if initialized is None:
        print("First time for", current_thread().name)
        threadLocal.initialized = True
    else:
        print("Welcome back", current_thread().name)

wont_work(); wont_work()

Wird zu dieser Ausgabe führen:

First time for MainThread
First time for MainThread

Multiprozessor-Modul

Alle globalen Variablen sind threadlokal, da das multiprocessingModul für jeden Thread einen neuen Prozess erstellt.

Betrachten Sie dieses Beispiel, in dem der processedZähler ein Beispiel für den lokalen Thread-Speicher ist:

from multiprocessing import Pool
from random import random
from time import sleep
import os

processed=0

def f(x):
    sleep(random())
    global processed
    processed += 1
    print("Processed by %s: %s" % (os.getpid(), processed))
    return x*x

if __name__ == '__main__':
    pool = Pool(processes=4)
    print(pool.map(f, range(10)))

Es wird ungefähr so ​​ausgegeben:

Processed by 7636: 1
Processed by 9144: 1
Processed by 5252: 1
Processed by 7636: 2
Processed by 6248: 1
Processed by 5252: 2
Processed by 6248: 2
Processed by 9144: 2
Processed by 7636: 3
Processed by 5252: 3
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

... natürlich variieren die Thread-IDs und die Anzahl für jede und jede Reihenfolge von Lauf zu Lauf.

mbells
quelle
9
"Beachten Sie, dass das Threading-Modul das reguläre Konzept von Threads verwendet (die Zugriff auf die globalen Prozessdaten haben), diese jedoch aufgrund der globalen Interpretersperre nicht allzu nützlich sind." Ist dies schwerwiegend? Wenn ich dies richtig lese, ist dies äußerst irreführend, da Threads immens nützlich und kritisch sind, ob GIL oder nicht.
Zzzeek
2
Die Funktion wont_work ist falsch, aber nicht, weil threading.local "im globalen Bereich verwendet werden muss". Der Code verwendet vielmehr eine lokale Variable (das threading.local-Objekt) und erwartet, dass sie Werte über Aufrufe hinweg beibehält. So verhalten sich lokale Variablen nicht (bei einem einfachen Diktat tritt das gleiche Problem auf).
Paul Moore
1
@zehelvion Sie sind nützlich, um mehrere Funktionen gleichzeitig auszuführen.
Zzzeek
@zzzeek Aber Prozesse in Python machen dasselbe? Nein, was ist der Unterschied, außer dass Sie dieselben Globals teilen oder einzigartige Globals haben?
AturSams
4
Können Sie bitte fett schreiben: "Eine wichtige Sache, die leicht übersehen wird: Ein threading.local () -Objekt muss nur einmal erstellt werden, nicht einmal pro Thread oder einmal pro Funktionsaufruf" :) - Ich dachte, ich würde verrückt!
Stadt
23

Thread-lokaler Speicher kann einfach als Namespace betrachtet werden (mit Werten, auf die über die Attributnotation zugegriffen wird). Der Unterschied besteht darin, dass jeder Thread transparent seine eigenen Attribute / Werte erhält, sodass ein Thread die Werte eines anderen Threads nicht sieht.

Wie bei einem normalen Objekt können Sie mehrere threading.localInstanzen in Ihrem Code erstellen . Dies können lokale Variablen, Klassen- oder Instanzmitglieder oder globale Variablen sein. Jeder ist ein separater Namespace.

Hier ist ein einfaches Beispiel:

import threading

class Worker(threading.Thread):
    ns = threading.local()
    def run(self):
        self.ns.val = 0
        for i in range(5):
            self.ns.val += 1
            print("Thread:", self.name, "value:", self.ns.val)

w1 = Worker()
w2 = Worker()
w1.start()
w2.start()
w1.join()
w2.join()

Ausgabe:

Thread: Thread-1 value: 1
Thread: Thread-2 value: 1
Thread: Thread-1 value: 2
Thread: Thread-2 value: 2
Thread: Thread-1 value: 3
Thread: Thread-2 value: 3
Thread: Thread-1 value: 4
Thread: Thread-2 value: 4
Thread: Thread-1 value: 5
Thread: Thread-2 value: 5

Beachten Sie, wie jeder Thread seinen eigenen Zähler verwaltet, obwohl das nsAttribut ein Klassenmitglied ist (und daher von den Threads gemeinsam genutzt wird).

Das gleiche Beispiel hätte eine Instanzvariable oder eine lokale Variable verwenden können, aber das würde nicht viel zeigen, da es dann keine Freigabe gibt (ein Diktat würde genauso gut funktionieren). Es gibt Fälle, in denen Sie threadlokalen Speicher als Instanzvariablen oder lokale Variablen benötigen, diese sind jedoch relativ selten (und ziemlich subtil).

Paul Moore
quelle
Eine globale Klasse mit einem Klassenattribut - interessant; Ich werde sehen, ob das auch das Problem löst, das ich hatte.
Ethan Furman
Andererseits ist ein einfaches globales Objekt, das beim Programmstart einmal initialisiert wurde, häufig die einfachste Lösung. Es ist einfach nicht der Fall , dass Sie brauchen zu tun , dass - wie bei jeder Variable, es hängt von der Anwendung.
Paul Moore
Wo ich Python jetzt professionell benutze, mache ich das schon lange nicht mehr. Da es sich jedoch nsum ein Klassenmitglied handelt, sollten wir es nicht als verwenden Worker.ns? Ich bin mir bewusst, dass der aktuelle Code funktioniert, da er self.nsals Getter das gleiche Ergebnis liefert wie Worker.ns, aber als Best Practice, die verwirrend erscheint (und in einigen Fällen fehleranfällig sein kann - wenn Sie dies tun, self.ns = ...wird das Klassenmitglied nicht geändert, sondern ein erstellt neues Feld auf Instanzebene). Was denken Sie?
Guyarad
Die Klasse zu benutzen oder selfist zu einem gewissen Grad weitgehend eine Frage des Stils, denke ich. Der Vorteil der Verwendung selfbesteht darin, dass sie mit Unterklassen funktioniert, bei denen der Klassenname nicht hart codiert wird. OTOH, es hat den Nachteil, dass es möglich ist, die Klassenvariable versehentlich mit einer Instanzvariablen zu beschatten, wie Sie sagen.
Paul Moore
17

Wie in der Frage erwähnt, gibt Alex Martelli hier eine Lösung . Mit dieser Funktion können wir eine Factory-Funktion verwenden, um einen Standardwert für jeden Thread zu generieren.

#Code originally posted by Alex Martelli
#Modified to use standard Python variable name conventions
import threading
threadlocal = threading.local()    

def threadlocal_var(varname, factory, *args, **kwargs):
  v = getattr(threadlocal, varname, None)
  if v is None:
    v = factory(*args, **kwargs)
    setattr(threadlocal, varname, v)
  return v
Casebash
quelle
1
Wenn Sie dies tun, ist das, was Sie wirklich wollen, wahrscheinlich defaultdict + ThreadLocalDict, aber ich glaube nicht, dass es eine Standardimplementierung davon gibt. (defaultdict sollte wirklich Teil von dict sein, z. B. dict(default=int), wodurch die Notwendigkeit eines "ThreadLocalDefaultDict" entfällt.)
Glenn Maynard
1
@Glenn, das Problem dabei dict(default=int)ist, dass der dict()Konstruktor kwargs aufnimmt und sie dem Diktat hinzufügt. Wenn dies implementiert wäre, könnten die Benutzer keinen Schlüssel mit dem Namen "Standard" angeben. Aber ich denke tatsächlich, dass dies ein kleiner Preis für eine Implementierung ist, wie Sie sie zeigen. Schließlich gibt es andere Möglichkeiten, einem Diktat einen Schlüssel hinzuzufügen.
Evan Fosmark
@Evan - Ich stimme zu, dass dieses Design besser wäre, aber es würde die Abwärtskompatibilität brechen
Casebash
1
@Glenn, ich verwende diesen Ansatz für viele threadlokale Variablen, die NICHT vorhanden sind defaultdict , wenn Sie das meinen. Wenn Sie meinen, dass dies eine ähnliche Schnittstelle hat wie das, was Sie haben defaultdictSOLLTEN (optionale Positions- und benannte Argumente für die Factory-Funktion bereitstellen: Jedes Mal, wenn Sie einen Rückruf speichern können, sollten Sie in der Lage sein, optional Argumente dafür zu übergeben! -), dann sorta, außer dass ich normalerweise verschiedene Fabriken und Argumente für verschiedene Varnamen verwende UND der Ansatz, den ich gebe, auch unter Python 2.4 gut funktioniert (fragen Sie nicht ...! -).
Alex Martelli
@Casebash: Sollte sich der Aufruf threadlocal = threading.local()nicht innerhalb der threadlocal_var()Funktion befinden, damit er den lokalen Wert für den Thread erhält, der ihn aufruft?
Martineau
5

Kann auch schreiben

import threading
mydata = threading.local()
mydata.x = 1

mydata.x existiert nur im aktuellen Thread

Casebash
quelle
4
Warum bearbeiten Sie nicht einfach Ihre Frage, anstatt diese Art von Code in eine eigene Antwort zu setzen?
Evan Fosmark
3
@Evan: Weil es zwei grundlegende Ansätze gibt, die wirklich getrennte Antworten sind
Casebash
3

Meine Art, einen lokalen Thread-Speicher über Module / Dateien hinweg zu erstellen . Folgendes wurde in Python 3.5 getestet -

import threading
from threading import current_thread

# fileA.py 
def functionOne:
    thread = Thread(target = fileB.functionTwo)
    thread.start()

#fileB.py
def functionTwo():
    currentThread = threading.current_thread()
    dictionary = currentThread.__dict__
    dictionary["localVar1"] = "store here"   #Thread local Storage
    fileC.function3()

#fileC.py
def function3():
    currentThread = threading.current_thread()
    dictionary = currentThread.__dict__
    print (dictionary["localVar1"])           #Access thread local Storage

In Datei A starte ich einen Thread, der eine Zielfunktion in einem anderen Modul / einer anderen Datei hat.

In Datei B habe ich eine lokale Variable festgelegt, die ich in diesem Thread haben möchte.

In Datei C greife ich auf die lokale Thread-Variable des aktuellen Threads zu.

Drucken Sie außerdem einfach die Variable 'dictionary' aus, damit Sie die verfügbaren Standardwerte wie kwargs, args usw. Sehen können .

Shivansh Jagga
quelle