Seite 1 von 1

Wann ist es Threadsafe?

Verfasst: Samstag 26. April 2008, 10:25
von sma
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

Verfasst: Samstag 26. April 2008, 10:33
von EyDu
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.

Verfasst: Samstag 26. April 2008, 10:42
von gerold
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
:-)

Verfasst: Samstag 26. April 2008, 10:53
von 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.

Verfasst: Samstag 26. April 2008, 11:21
von Y0Gi
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.

Verfasst: Samstag 26. April 2008, 11:24
von EyDu
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.

Verfasst: Sonntag 27. April 2008, 08:46
von sma
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

Verfasst: Sonntag 27. April 2008, 09:18
von EyDu
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.

Verfasst: Sonntag 27. April 2008, 12:22
von veers
Trellis ist ein anderer Ansatz. Ich kam damit jedoch irgend wie nicht klar. :wink:

Verfasst: Sonntag 27. April 2008, 21:03
von Leonidas
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.