Wann ist es Threadsafe?

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Angenommen, ich habe einen multithreaded HTTP-Server in Python, bei dem jeder Request in einem eigenen Thread verarbeitet wird. Alle durchlaufen Code wie diesen, wobei die beteiligten Objekte global sind:

Code: Alles auswählen

account1.withdraw(amount)
account2.deposite(amount)
Dieser Code ist offensichtlich nicht threadsafe. Doch Python hat einen GIL. Wann erlaubt dieser Threadwechsel? Nach jedem Bytecode? Nach jedem Befehl? Nur wenn IO-Operationen die Kontrolle abgeben? Ein Blick in die Doku des threading-Moduls hat mir nicht geholfen. Ich frage mich gerade, ob der GIL Code wie den da oben threadsafe macht oder nicht.

Stefan
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Alle 100 (weiß nicht, ob der Wert noch stimmt) Bytecodeinstructions und natürlich bei blockierenden Aufrufen (IO, Locks). Zumindest bei CPython, bei anderen Implementierungen muss dies natürlich nicht gewährleistet sein.

Ich würde es aber gleich vernünftig threadsicher machen und mich nicht auf solche Spielchen einlassen. So groß ist der Aufwand dann nun wirklich nicht.
Benutzeravatar
gerold
Python-Forum Veteran
Beiträge: 5555
Registriert: Samstag 28. Februar 2004, 22:04
Wohnort: Oberhofen im Inntal (Tirol)
Kontaktdaten:

Hallo Stefan!

Threadsichere Funktionen sind keine Hexerei:

Code: Alles auswählen

>>> import threading
>>> my_lock = threading.Lock()
>>> def my_global_function():
...     my_lock.acquire()
...     try:
...         print "Schritt 1"
...         print "Schritt 2"
...     finally:
...         my_lock.release()
...
mfg
Gerold
:-)
http://halvar.at | Kleiner Bascom AVR Kurs
Wissen hat eine wunderbare Eigenschaft: Es verdoppelt sich, wenn man es teilt.
BlackJack

Die 100 Bytecodes bis zum nächsten Threadwechsel kann man mit `sys.setcheckinterval()` verändern, das ist also keine verlässliche Grösse. Ausserdem können `withdraw()` und/oder `deposit()` irgend etwas benutzen, was einen Threadwechsel erlaubt. Ich würde mich da auch unter CPython auf nichts verlassen und explizit Sperrsynchronisation benutzen.
Y0Gi
User
Beiträge: 1454
Registriert: Freitag 22. September 2006, 23:05
Wohnort: ja

gerold hat geschrieben:

Code: Alles auswählen

>>> import threading
>>> my_lock = threading.Lock()
>>> def my_global_function():
...     my_lock.acquire()
...     try:
...         print "Schritt 1"
...         print "Schritt 2"
...     finally:
...         my_lock.release()
...
Erfreulicherweise kann ein Lock auch als Context-Manager agieren und dadurch das `with`-Statement verwendet werden (Quelle):

Code: Alles auswählen

from __future__ import with_statement
import threading

my_lock = threading.Lock()

def my_global_function():
    with my_lock:
        print "Schritt 1"
        print "Schritt 2"
`.acquire()` und `.release()` werden dann entsprechend automatisch beim Betreten und Verlassen des Blocks aufgerufen. Das nenne ich mal eine sehr angenehme Verbesserung.
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Und mit dem with-Statement geht es noch etwas kürzer:

Code: Alles auswählen

>>> import threading
>>> my_lock = threading.Lock()
>>> def my_global_function():
...     with my_lock:
...         print "Schritt 1"
...         print "Schritt 2"
edit: ja ja, zu spät...

Edit (Leonidas): Restliche Diskussion in "With-Statement" abgetrennt.
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Mir ist bekannt, wie man Programme threadsafe machen kann. Mir ist jedoch auch bewusst, dass das generell eine sehr schwere und sehr fehleranfällige Sache ist, da man leicht einen Fall vergessen kann. Daher wollte ich mal nachfragen, ob man die Eigenheiten von Python für sich nutzen kann. Die Antwort war nein. Das wollte ich lernen.

Ich habe mir so ein (leider noch halbgares) Transaktionskonzept für Objekte ausgedacht, welches ich vor einem Einsatz in einem HTTP-Server natürlich transaktionssicher machen muss. Bereits ein einfaches `a += 1` müsste sonst über einen Lock synchronisiert werden. Locke ich nur jeweils die Objekte, kann es zu deadlocks kommen. Ich möchte daher ein "copy on write" machen, damit sich threads nicht gegenseitig beeinflussen können. Gleichzeitig möchte ich, dass nur ein Thread zur Zeit ändern kann.

Hier ist ein kurzer Ausschnitt:

Code: Alles auswählen

class Transaction(threading.local):
    def owns(self, model):
        return model._t_owner is self.thread
	
    def get(self, model, name):
        if name.startswith('_t_'):
            return model.__dict__[name]
        if self.owns(model):
            return model._t_new[name]
        return model._t_old[name]
Am einfachsten wäre ein globaler Lock um `get` herum, aber das bremst das System unnötig aus. Man kann besser werden, aber der Preis ist die Gefahr, einige Fälle zu übersehen. Systeme wie Erlang haben schon ihre Vorteile...

Stefan
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Ich hatte mal so ein ähnliches Problem. Dazu habe ich wie du bereits auch schon, eine Transaktions-Klasse eingeführt. Jede instanz hatte einen eigenen Lock. Wird eine Instanz erstellt, dann wird die Transaktion in einen Queue fuer Transaktionen eingehängt, welche von einem eigenen Thread behandelt wurden. Der aufrufende Thread wurde natürlich durch das Lock-Objekt in der Transaktion gebremst.

Ist die Transaktion durch den abarbeiteten Thread durchgeführt, so wurden entsprechende Ergebnisse in die Transaktion übergeben und der Lock gelöst. Damit konnte der Aufrufer dann mit samt den Ergebnissen weiterlaufen.

Durch die eingesparten Locks im Worker-Thread konnte ich einiges an Performance hinzugewinnen. Eventuell lässt sich das System bei dir, in vielleicht etwas angepasster Form, auch anwenden.
Benutzeravatar
veers
User
Beiträge: 1219
Registriert: Mittwoch 28. Februar 2007, 20:01
Wohnort: Zürich (CH)
Kontaktdaten:

Trellis ist ein anderer Ansatz. Ich kam damit jedoch irgend wie nicht klar. :wink:
[url=http://29a.ch/]My Website - 29a.ch[/url]
"If privacy is outlawed, only outlaws will have privacy." - Phil Zimmermann
Leonidas
Python-Forum Veteran
Beiträge: 16025
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

veers hat geschrieben:Trellis ist ein anderer Ansatz. Ich kam damit jedoch irgend wie nicht klar. :wink:
Trellis ist tatsächlich ein interessanter Ansatz, aber ich finde da fehlt noch irgendwie ein konkreter Einsatzzweck wo man sagen könnte "Ha, dafür wäre Trellis optimal". Naja, wenn man vom CLOS kommt, dann wird man die Cells mögen, aber ich habe beim besten Willen nicht herausgefunden, wozu mir persönlich Trellis behilflich sein könnte.
My god, it's full of CARs! | Leonidasvoice vs (former) Modvoice
Antworten