threading problem - Übergabe einer Liste aus dem Thread

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
Benutzeravatar
ts7343
User
Beiträge: 69
Registriert: Mittwoch 5. Mai 2010, 13:48

Hallo,

kann mal jemand von den Threading Experten kurz in meinen Beispiel-Code schauen?

Das ganze soll eigentlich in einem GUI ablaufen aber ich hab es mal auf das Wesentliche heruntergebrochen:
Ein Thread soll während seiner Laufzeit eine Liste erstellen, diese wird dann zwischendurch abgefragt.

Wenn man nun dieses Script laufen lässt sieht man, dass in der Funktion run_app die Liste self.history abgefragt
wird, die durch den Thread ständig aktualisiert wird.

Jedoch sieht man an dem Output, das beide Listen, die aktuelle im Thread und die durch run_app abgerufene, nicht gleich sind.

Warum ist das so?
Wie kann ich das synchronisieren?
Ist der Ansatz falsch und sollte ich lieber über einen temporären File gehen?

Habt ihr da eine Idee für mich

Vielen Dank
Lutz


Code: Alles auswählen

#!/usr/bin/env python

import time, threading

# ------------------------------------------------------------------------------


class HistThreading(threading.Thread):
  
   def __init__(self):
      threading.Thread.__init__(self)
      self.history = []
      self.running = False
   
   
   def run(self):
      self.running = True
      counter = 0      
      while self.running == True:
         counter = counter + 1
         print "thread run: ", str(counter)
         self.get_history_from_threading()        
         time.sleep(1)
         if counter >= 20:
            self.running = False
      

   def get_history_from_threading(self):
      self.history.append(int(1))
      print "thread get_history: ", len(self.history), self.history
      return self.history


# ------------------------------------------------------------------------------



class BaseTest(HistThreading):
   
   def __init__(self):
      self.history = []
      
   def any_function(self):
      print "base: ", self.history


# ------------------------------------------------------------------------------


class AppTest(BaseTest):

   def __init__(self): 
      self.history = []          
      self.thread_01 = HistThreading()
      self.thread_01.start()
      
   def run_app(self):
      i = 0
      while i < 10:
         i = i + 1
         app_history = self.get_history_from_threading()
         print "app: ", i, len(app_history), app_history
         time.sleep(3)
      

# ------------------------------------------------------------------------------


if __name__ == "__main__":

   a = AppTest()
   a.run_app()

BlackJack

@ts7343: Du hast da zu viele `history`\s. Und zu viele ``while``-Schleifen. Wenn man solche Schleifen statt ``for`` verwendet, sollte man noch die Finger von Nebenläufigkeit lassen.

Wo liegt denn Dein Problem? Natürlich sind die nicht gleich, denn jedes mal wenn Du die *eine* `history`-Liste die tatsächlich mit Daten gefüllt wird, abfragst, wird beim Abfragen ein Element hinzugefügt. Damit ist es unmöglich, dass zwei verschiedene Abfragen den gleichen Listeninhalt liefern. Das ist doch offensichtlich‽

`HistThreading.history` ist die einzige Liste mit der Du etwas machst.

`BaseTest` ist sinnfrei weil keine der beiden Methoden in dem Quelltext verwendet werden, damit kann man die Klasse für ein Minimalbeispiel ersatzlos streichen.

`AppTest.history` wird nicht verwendet.

Warum wird das `Thread`-Objekt an den Attributnamen `thread_01` gebunden? Nummerierte Namen sind oft ein Zeichen auf einen schlechten Entwurf.
Benutzeravatar
ts7343
User
Beiträge: 69
Registriert: Mittwoch 5. Mai 2010, 13:48

Hallo BlackJack,

vielen Dank, dass du mal draufgeschaut hast, die "while"-Schleife ist eigentlich nur deshalb, da es später mal im GUI ein while sein muss, bis der User den Quit Button betätigt, aber hast schon Recht, hierfür reicht die "for"-Schleife. Der thread_01 ist erstmal nur willkürlich gewählt, da es nur einen Thread geben wird.

Ich hab mal die 'BaseTest' entfernt und noch etwas vereinfacht, HistThreading.get_history für die pure Abfrage.
aber auch hier funktioniert es nicht. Nun ist selbst HistThreading.history leer wenn HistThreading.get_history einen print macht.

Ist das alles dadurch, dass ich eine AppTest.history = [ ] definiere?
Aber wenn ich in AppTest die Definition von self.history = [ ] weglasse bekomme ich:

AttributeError: 'AppTest' object has no attribute 'history'

Also muss ich es ja definieren.

Wie kann ich das anpacken?

Hier der noch mehr abgespeckte Code:

Code: Alles auswählen

#!/usr/bin/env python

import time, threading

# ------------------------------------------------------------------------------

class HistThreading(threading.Thread):
 
   def __init__(self):
      threading.Thread.__init__(self)
      self.history = []         
   
   def run(self):
      for i in range(10):
         self.history.append(1)
         print "hist set", len(self.history), self.history
         time.sleep(1)   

   def get_history(self):
      print "hist get: ", self.history
      return self.history

# ------------------------------------------------------------------------------

class AppTest(HistThreading):

   def __init__(self):
      self.history = []          
      self.thread_01 = HistThreading()
      self.thread_01.start()
     
   def run_app(self):
      for i in range(5):  
         app_history = self.get_history()
         print "app: ", i, len(app_history), app_history
         time.sleep(3)
     
# ------------------------------------------------------------------------------

if __name__ == "__main__":

   a = AppTest()
   a.run_app()
BlackJack

@ts7343: Es gibt in Deinem Programm zwei Objekte: Ein Examplar von `HistThreading` und eines von `AppTest`. Beide haben ein `history`-Attribut. Das `HistThreading`-Exemplar ändert seine Liste und das `AppTest`-Exemplar gibt seine leere Liste aus die nirgends verändert wird.

Vom Umstand das `AppTest` von `HistThreading` abgeleitet ist, machst Du keinen Gebrauch. Genau das willst Du aber wahrscheinlich. Also wen mit dem `AppTest.thread_01`-Attribut und stattdessen die `__init__()` von der Basisklasse aufrufen und auch weg mit der Zuweisung an `AppTest.history` in der `AppTest.__init__()`, denn das Attribut wird in der `__init__()` der Basisklasse schon erstellt.

Jetzt bleibt semantisch nur noch die Frage zu klären ob ein `AppTest`-Objekt wirklich ein `HistThreading`-Objekt sein sollte, denn Vererbung ist für „ist-ein(e)” gedacht. Oder ob Kompisition hier nicht angebrachter wäre.

Und das nächste Problem ist dann die Integration in eine GUI, denn üblicherweise sind GUI-Toolkits nicht thread-sicher und man darf mit der GUI nur aus dem Hauptthread interagieren.
Benutzeravatar
ts7343
User
Beiträge: 69
Registriert: Mittwoch 5. Mai 2010, 13:48

@BlackJack:
ok, meinst du damit, ich soll auch in der __init__ Funktion die Basisklasse erwähnen? so z.B.:

Code: Alles auswählen

class AppTest(HistThreading):
   def __init__(self, HistThreading):
aber auch dann kennt er das HistThreading.history noch nicht, ebenfalls mit:

Code: Alles auswählen

class AppTest(HistThreading):
   def __init__(self, HistThreading):
      HistThreading.__init__()
funktioniert es nicht, ich glaube ich stosse gerade an meine OOP-Grenzen,
somit weiss ich leider auch nicht was Komposition bedeutet.

Hast du ein bisschen Code für mich wie ich den thread_01.run starte wenn ich AppTest von HistThreding ableite?
BlackJack

@ts7343: Man ruft in abgeleiteten Klasse in deren `__init__()` normalerweise die `__init__()` von der Basisklasse auf. Und zwar direkt über die Basisklasse und nicht in dem man die Basisklasse noch einmal als Argument entgegen nimmt. Das würde ja bedeuten man müsste beim Erstellen von Exemplaren immer die eventuell vorhandene(n) Basisklasse(n) mit übergeben.

Das sind in der Tat OOP-Grundlagen. Und Du *machst* das ja schon bei der `HistThread` mit *deren* Basisklasse. Wo liegt jetzt das Problem das auf die andere Vererbung zu übertragen‽

Wenn Du `AppTest` von `HistThreading` ableitest, dann startest Du `thread_01.run` gar nicht, denn dann braucht man das Attribut nicht, denn ein `AppTest`-Objekt ist gleichzeitig ein `HistThreading`-Objekt und hat damit all dessen Methoden und Attribute. Und auch alles von `Thread`, denn das erbt `HistThreading` seinerseits von `threading.Thread`. Genau das ist der Sinn von Vererbung. Und weil `AppTest` selbst ein `HistThreading`- und damit auch ein `Thread`-Objekt *ist*, modelliert man mit Vererbung „Untertyp ist-ein(e) Basistyp”-Beziehungen.

Typisches Beispiel: `Auto` abgeleitet von `Fahrzeug`, was Sinn macht weil ein Auto ein Fahrzeug ist. `Fahrzeug` mag eine Methode `starten()` haben, die auch beim `Auto` sinnvoll ist. Eine Klasse `Autofahrer` mag auch eine Methode `starten()` haben, die ein `Auto` startet, aber deshalb sollte man nicht `Autofahrer` von `Auto` ableiten, weil ein `Autofahrer` kein `Auto` oder `Fahrzeug` *ist*.

Wenn diese Beziehung nicht gilt, arbeitet man mit Komposition oder Aggregation, also setzt ein Objekt aus anderen zusammen. Der `Autofahrer` könnte zum Beispiel ein `Auto`-Exemplar als Attribut `auto` besitzen und wenn man `starten()` auf dem Fahrer aufruft, ruft diese Methode `starten()` auf dem `auto`-Attribut auf. Was seinerseits vielleicht direkt die geerbte `Fahrzeug.starten()` ist, oder diese vielleicht als Teil des eigenen Ablaufs aufruft.

Dein Beispiel „korrigiert”:

Code: Alles auswählen

from threading import Thread
from time import sleep


class HistoryThread(Thread):
    def __init__(self):
        Thread.__init__(self)
        self.history = list()
    
    def run(self):
        for _ in xrange(10):
            self.history.append(42)
            print 'history', len(self.history), self.history
            sleep(1)


class Application(HistoryThread):
    def __init__(self):
        HistoryThread.__init__(self)
        self.start()
    
    def __call__(self):
        for i in xrange(5):
            app_history = self.history
            print 'app:', i, len(app_history), app_history
            sleep(3)


def main():
    application = Application()
    application()


if __name__ == '__main__':
    main()
Den simplen „Getter” für `history` habe ich weg gelassen, weil man so etwas in Python eigentlich nicht macht.

Das Programm hat allerdings das Problem, dass es nicht thread-sicher ist. Zwischen Abfragen und Ausgeben der Länge der Liste könnte es passieren, dass der Thread ein weiteres Element hinzufügt und damit die ausgegebene Länge nicht zur danach ausgegebenen Liste passt. Man müsste die gemeinsam genutzen Daten also mit einem entsprechenden Sperrmechanismus schützen. Und wenn man wie hier eine komplette Datenstruktur zurück gibt, wird es entweder kompliziert, oder man gibt eine Kopie der Datenstruktur zurück.
Benutzeravatar
ts7343
User
Beiträge: 69
Registriert: Mittwoch 5. Mai 2010, 13:48

@BlackJack:
das sieht gut aus, das mit der "42" natürlich auch! Vielen Dank!

Der "Getter" hat sich dadurch natürlich auch erledigt weil man direkt an die Daten kommt.

self.start() sieht besonders sauber aus.

Das Thema Komposition muss ich mir wahrscheinlich erst mal in Ruhe zu Gemüte führen.

Ich werd mal versuchen das in die GUI Application zu implementieren, das mit der Sicherheit ist in meinem Falle wahrscheinlich nicht das Problem, ich nehme was ich kriege und werde notfalls nachbearbeiten wenn die Daten nicht genau passen, aber hast schon recht, ein Spreemechanismus wäre bestimmt sauberer.
Antworten