cPickle Datenbank + threading

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.
Brainsucker
User
Beiträge: 68
Registriert: Mittwoch 16. November 2011, 23:20

Hallo Leute,



trotz der zigtausenden Programmierbeispiele zu threading im World-Wide-Web habe ich immer noch ziemliche startschwierigkeiten damit.

Ganz simple Funktionen zu threaden stellt ja kein Problem dar. Allerdings bin ich nun dabei das ganze über die Objektorientierte ebene anzugehen. Und da klennt es wohl ein "wenig".

Also ich will eine cPickle Datenbank (mit großer Datenmenge) möglichst laggfrei über cPickle laden und speichern können.

Dazu habe ich mir gedacht erstelle ich mir folgende 3 Klassen:


Hier die erste Klasse, mit der ich die Datenbank über einen Thread laden kann:

Code: Alles auswählen

class DBLoader(threading.Thread):
	def __init__(self):
		threading.Thread.__init__(self)
		
	def run(self):
		self.db_data = cPickle.load("pfad zur datenbank und so... tut dem thema nichts zur sache^^")
Hier die zweite Klasse, mit der ich die Datenbank über einen Thread speichern kann:

Code: Alles auswählen

class DBSaver(threading.Thread):
	def __init__(self):
		threading.Thread.__init__(self)
		
	def run(self):
		with open(dbpath, 'wb') as f:
			cPickle.dump(data, f)
Und die dritte Klasse, über diese ich die beiden oberen Klassen "manage" und die die beiden Threads über einen Lock() verriegelt:

Code: Alles auswählen

class DBManager:
	def __init__(self):
		self.l = threading.Lock()
	def saveDB(self):
		with self.l:
			saver = DBSaver()
			saver.start()
	
	def loadDB(self):
		with self.l:
			loader = DBLoader()
			loader.start()
Nun mal als aller erstes die Frage:
Macht die sache so wie ich es mir gedacht hat überhaupt einen Sinn und wenn ja: Gäbe es eventuell eine bessere Variante das zu lösen?

Meine nächste Frage wäre, wie ich denn nun genau auf die Daten des Ladenden Threads zugreife (nachdem das laden vollständig abgeschlossen ist)?

Hoffe ihr könnt mir das große Fragezeichen ein wenig in ein Ausrufezeichen "verwandeln"^^

MfG
Stefan
BlackJack

@Brainsucker: Das macht so keinen Sinn. Die Klassen zum laden und speichern sind unnötig kompliziert, da würde jeweils eine Funktion genügen, denn die bestehen ja nur auch der `run()`-Methode. Die `__init__()` ist in der Form sinnfrei, denn man hätte genau den selben Effekt, wenn man sie einfach weglassen würde.

An die Daten vom Loader kommst Du so gar nicht, denn der ist nur lokal in `loadDB` bekannt. Der Saver nimmt seine Daten auf magische Weise von irgend wo her, was auch sehr undurchsichtig und damit unsauber ist.

Das mit dem Lock ist sinnfrei, denn das ist ja nur so lange gesperrt wie es dauert den jeweiligen Thread zu starten, aber während er läuft ist es wieder frei.

Zumindest unter CPython kann es Dir übrigens passieren, dass dort gar nichts parallel läuft, denn die Funktionen aus `cPickle` müssten dazu das Global Interpreter Lock (GIL) freigeben, und das halte ich für unwahrscheinlich denn dann wären die Daten, die damit gepickelt werden, nicht mehr thread-sicher.
deets

Ergaenzend zu BlackJack's Ausfuehrungen - wenn du einen Objekt-Store haben willst, dann schau dir doch die ZODB an. Ich wuerde allerdings dazu raten, dort wenn moeglich nur simple Datenstrukturen, und nicht Objekte eigener Klassen drin zu speichern.
Brainsucker
User
Beiträge: 68
Registriert: Mittwoch 16. November 2011, 23:20

Hey,


danke euch beiden für die Antworten.

Hab die Klasse nun ein wenig überarbeitet und nun schaut diese so aus:

Code: Alles auswählen

class DBManager:
	def __init__(self, dbpath):
		self.dbpath = dbpath
		self.db_data = None
	
	def _saveDB(self, data):
		with open(self.dbpath) as f:
			cPickle.dump(f, data)
	
	def _loadDB(self):
		if not os.path.isfile(self.dbpath):
			raise IOError, 'Der angegebene Pfad existiert nicht.'
			
		with open(self.dbpath, 'rb') as f:
			self.db_data = cPickle.load(f)
			
	def load(self):
		t = threading.Thread(target=self._loadDB)
		t.start()
		
	def save(self):
		t = threading.Thread(target=self._saveDB)
		t.start()

Im gegensatz zum anderen Beispiel vom ersten Post, müsste diese Version sogar lauffähig sein. :)

Was ich nun noch erreichen will, ist dass die Datenbank keinesfalls gleichzeitig gespeichert und geladen werden kann. Also darf quasi immer nur einer der beiden threads laufen.

Könnt ihr mir dazu irgendwelche anregungen geben? :)
Benutzeravatar
sparrow
User
Beiträge: 4187
Registriert: Freitag 17. April 2009, 10:28

Ist es denn überhaupt sinnvoll das in einen Thread auszulagern?

Was genau soll denn dein Hauptthread in der Zeit machen? Bzw. was soll der Benutzer in der Zeit machen können?

Es nützt ja nichts, wenn das Programm zwar nicht "hängt" während die Datenbank geöffnet wird, aber ohne Datenbasis eh nicht gearbeitet werden kann.
BlackJack

@Brainsucker: Eine Sperre in Form eines `threading.Lock` wäre die Anregung. Und damit solltest Du dann auch noch den Zugriff auf `db_data` absichern und vor dem Speichern vielleicht eine Kopie erzeugen, die dann gespeichert wird. Ansonsten kann man eventuell während des Speicherns die Daten verändern, was zu Inkonsistenzen führen kann.

Und dann solltest Du auf jeden Fall mal nachmessen ob das überhaupt einen Vorteil bringt. Wie schon gesagt vermute ich bei CPython trotz Thread eine sequentielle Abarbeitung und damit keinen Geschwindigkeitsgewinn. Dort würdest Du also einfach nur die Komplexität des Programms erhöhen, ohne einen Vorteil davon zu haben.
Brainsucker
User
Beiträge: 68
Registriert: Mittwoch 16. November 2011, 23:20

Danke für die weiteren Hinweise :)

Ich ziele bei der Aktion mit den threads nicht auf einen Geschwindigkeits gewinn hinaus.

Wichtig ist mir einfach nur, dass das Laden und Speichern der Datenbank im Hintergrund passiert. Die Zeitdauer wie lange dies letztendlich benötigt ist mir vollkommen egal.

Nochmal eine kleine modifikation der Klasse.
Diesmal mit threading.Lock() (ich hoffe es ist so richtig)

Code: Alles auswählen

class DBManager:
	def __init__(self, dbpath):
		self.dbpath = dbpath
		
		
		self.l = threading.Lock()
	
	def _saveDB(self, data):
		with self.l:
			self.db_data = data
			_data_copy = copy.deepcopy(self.db_data)
			with open(self.dbpath) as f:
				cPickle.dump(_data_copy, f)
	
	def _loadDB(self):
		if not os.path.isfile(self.dbpath):
			raise IOError, 'Der angegebene Pfad existiert nicht.'
			
		with self.l:
			with open(self.dbpath, 'rb') as f:
				self.db_data = cPickle.load(f)
			
	def load(self):
		t = threading.Thread(target=self._loadDB)
		t.start()
		
	def save(self):
		t = threading.Thread(target=self._saveDB)
		t.start()
deets

Wozu soll das copy.deepcopy gut sein? Das ist nicht thread-sicher, wenn also dein main-thread waehrend des speicherns die Daten veraendert, dann bekommst du da potentiell Probleme.

Ausserdem ist das speichern der Daten in der Instanz fragwuerdig. Entweder gehst du den Weg wie ZODB, und dein DB-Objekt "hat" die Daten, die du manipulierst, und dann speicherst.

Oder du reichst die Daten von aussen rein, und das Ding laesst sich reduzieren darauf, dump und load nebenlaeufig aufzurufen.

Aber dein hybrider Ansatz ist schnell Murks, denn dann weiss man nicht, welche Daten denn eigentlich die richtigen sind - die, die von aussen mal reingereicht wurden und in der Zwischenzeit vielleicht weiter manipuliert. Oder die, die aus dem DB-Objekt kommen.

Last but not least - gerade das Load ist ziemlicher Unsinn, denn ohne ein Event, dass der Ladevorgang beendet worden ist & die Daten nun zur Verfuegung stehen, ist das ganze ja eigentlich fast nicht benutzbar. Wenigstens eine Flagge/Condition/sonstewas solltest du da setzen, damit man von aussen periodisch pruefen kann, ob die Daten denn nun da sind.
Brainsucker
User
Beiträge: 68
Registriert: Mittwoch 16. November 2011, 23:20

Hey deets,


danke für deine Anregung. Das mit dem Condition klingt sehr sinnvoll. Werd mich mit den Conditions mal ein wenig außereinander setzen.

Zu dem bisher schon öfters angesprochenen ZODB kann ich leider nicht viel sagen, da ich mich damit nicht auskenne. Mit cPickle bin ich sehr zufrieden und es ist auch für relativ "große" Datemengen geeignet, da es recht schnell ist.

Ich glaube kaum, dass in diesem Fall eine andere Datenbankart (wie csv oder XML) geeigent wäre, da diese deutlich langsamer sind. (Oder irre ich mich da?)
Desshalb würde ich ganz gerne erstmal bei cPickle bleiben.
BlackJack

@Brainsucker: Wenn das Laden und Speichern im Hintergrund passieren *würde*, dann hättest Du ja einen Geschwindigkeitsgewinn und den *willst* Du auch haben, denn sonst verstehe ich nicht warum das im Hintergrund passieren soll!? Nur hat man diesen Gewinn bei CPython aller Wahrscheinlichkeit nach überhaupt nicht, weil das eben nicht im Hintergrund passiert wird, sondern weiterhin sequentiell. Das zu *überprüfen* solltest Du vielleicht tun *bevor* Du noch mehr Zeit drauf verwendest eine eventuell/wahrscheinlich nutzlose Komplexität zu basteln.
Brainsucker
User
Beiträge: 68
Registriert: Mittwoch 16. November 2011, 23:20

Also um den Hintergrund meines vorhabens zu erläutern und damit ein wenig Licht ins dunkel gerückt wird:

Das Script soll später einmal auf einem Gameserver laufen und *aktiv* ins Spielgeschehen eingreifen.

Allerdings sollen diese eingriffe möglichst Laggfrei erfolgen, da dies die Spielqualität beeinflusst.

Dazu gehört auch das speichern und laden der Datenbank. Aus diesem Grund MUSS dies im Hintergrund passieren um das Spielgeschehen nicht durch laggs zu beeinflussen.

Daher mache ich den ganzen Aufwand um die Daten via Threads laden und speichern zu können.
deets

@BrainSucker

Du hast immer noch nicht verstanden, was BlackJack meint - er will darauf hinaus, dass du erst mal pruefen sollst, ob deine Annahme ueber Parallelitaet dieser Operation ueberhaupt zutrifft. Oder ob es durch das Python GIL zwangsweise serialisiert wird.
Dav1d
User
Beiträge: 1437
Registriert: Donnerstag 30. Juli 2009, 12:03
Kontaktdaten:

@deets, Falls der/das GIL greift gibt es viele kleine "Laggs", die der Spieler wahrscheinlich nicht einmal merkt, falls man das Ganze aber ohne Threads anpackt, werden die Daten in einem Rutsch geladen/gespeichert, was zu einem größeren "Lagg" führt, welcher definitiv wahrgenommen wird.
the more they change the more they stay the same
BlackJack

@Dav1d: Das stimmt bei CPython nur falls Python-Bytecode ausgeführt wird oder wenn die in C implementierte `dump()`-Funktion das GIL frei gibt. Letzteres glaube ich wie gesagt nicht. Beim serialisieren kann potentiell Python-Bytecode ausgeführt werden, zum Beispiel wenn Exemplare angetroffen werden, welche sich mit Python-Code in die Serialisierung ”einmischen” (`__reduce__()` & Co). Dann kommt es wahrscheinlich darauf an, wie viel von solchem Code im Verhältnis zu solchen Objekten, die sich über in C implementierten Code serialisieren lassen, in den Daten vorkommen. Das sollte man aber *nachprüfen*. Denn wenn es ungünstig läuft ist der Aufruf der `dump()`-Funktion aus Sicht der Threads atomar oder hat zumindest deutlich blockierendere Wirkung auf andere Threads in denen Python-Bytecode ausgeführt wird, als sich Brainsucker das erhofft.

@Brainsucker: Was sind denn das für Daten und wie sieht das Zugriffsmuster aus? Ist es denn notwendig immer alle Daten zu laden und zu speichern?
Brainsucker
User
Beiträge: 68
Registriert: Mittwoch 16. November 2011, 23:20

BlackJack hat geschrieben:@Brainsucker: Was sind denn das für Daten und wie sieht das Zugriffsmuster aus? Ist es denn notwendig immer alle Daten zu laden und zu speichern?

Ja, leider ist es notwendig permanent Zugriff auf die komplette Datenbank zu haben und somit müssen stets alle Daten vorhanden sein.

Wenn dies nicht der fall wäre, könnte ich ja stattdessen viele kleine Datenbanken anlegen was die nutzung von threads erübrigen würde.

Naja hauptsächlich besteht die Datenbank wohl aus datetime.datetime Objekten, kurz gesagt für jeden Spieler genau ein datetime Objekt.
Da kann sich bei gut besuchten Servern ein relativ hohe Datenmenge anhäufen. Ich schätze so 600-1000 einträge werden bei manchen Servern locker pro TAG erreicht.

Daher ist es wie schon oben gesagt zwingend notwenig die sache in threads zu packen.
Dav1d
User
Beiträge: 1437
Registriert: Donnerstag 30. Juli 2009, 12:03
Kontaktdaten:

Naja 600-1000 ist nichts.

Als ich noch Plugins mit EventScripts geschrieben habe (für CounterStrike:Source), hat mein rank-Plugin (getestet bis 100k Einträge in einer shelve-db, welche auch pickle nutzt), keinen Lag verursacht. Und wie gesagt, 600-1000 ist nichts. Außerdem könntest du alle Daten im Speicher halten und z.B. zum Rundenende oder Beginn speichern, was wiederum, falls es Laggs geben sollte, das erträglich macht.
the more they change the more they stay the same
BlackJack

@Brainsucker: Ich weiss nicht ob wir das selbe unter dem Begriff „Datenbank“ verstehen. Wenn Du nicht auf den gesamten Datenbestand zugreifen musst, dann ist die Schlussfolgerung ganz sicher nicht, dass man viele kleine Datenbanken anlegt, sondern dass man nicht alles auf einmal in den Speicher lädt, sondern nur das was man benötigt. Aber eben aus *einer* Datenbank. Das was Du da machst — eine grosse Datenstruktur auf einen Schlag zu de-/serialisieren — würde ich auch nicht unbedingt als Datenbank bezeichnen. Ich weiss man kann den Begriff recht weit fassen, aber Datenbank impliziert für mich in der Regel mehr Daten als man im Speicher halten kann oder möchte und mindestens einen einfachen Key/Value-Store um auf die Daten per Schlüssel zuzugreifen.

Das mit den Threads ist sicher nicht zwingend notwendig, denn wie schon mehrfach gesagt, wird das Dein Problem zumindest in CPython mit an Sicherheit grenzender Wahrscheinlichkeit wegen dem GIL nicht lösen.

Du hast noch nicht wirklich etwas zur Beschaffenheit der Datenstruktur und zum Zugriffsmuster gesagt. Deshalb ist es schwer einen konkreten Vorschlag zu machen. Bei den Bordmitteln ist `shelve` als einfache Key/Value-Datenbank und `sqlite` als einfache SQL-Datenbank dabei. Ansonsten kann man sich ausserhalb der Standardbibliothek auch „grössere“ SQL-Datenbanksysteme wie MySQL oder PostgreSQL anbinden, oder eine der NoSQL-Datenbanken für die es eine Python-Anbindung gibt. ZODB oder Durus (falls das Projekt noch „lebt“) sind Objektdatenbanken für Python.
Brainsucker
User
Beiträge: 68
Registriert: Mittwoch 16. November 2011, 23:20

Dav1d hat geschrieben:Naja 600-1000 ist nichts.

Als ich noch Plugins mit EventScripts geschrieben habe (für CounterStrike:Source), hat mein rank-Plugin (getestet bis 100k Einträge in einer shelve-db, welche auch pickle nutzt), keinen Lag verursacht. Und wie gesagt, 600-1000 ist nichts. Außerdem könntest du alle Daten im Speicher halten und z.B. zum Rundenende oder Beginn speichern, was wiederum, falls es Laggs geben sollte, das erträglich macht.

uedi?!

Oder lieg ich da wohl falsch?

Ach ja und lese bitte mal richtig... Ich hab nicht von 600-1000 Einträgen gesprochen sondern von 600-1000 PRO TAG. Was dann mit der zeit eine "relativ" große Datenmenge für eine CSS cPickle Datenbank ergibt ;)
Brainsucker
User
Beiträge: 68
Registriert: Mittwoch 16. November 2011, 23:20

BlackJack hat geschrieben:@Brainsucker: Ich weiss nicht ob wir das selbe unter dem Begriff „Datenbank“ verstehen. Wenn Du nicht auf den gesamten Datenbestand zugreifen musst, dann ist die Schlussfolgerung ganz sicher nicht, dass man viele kleine Datenbanken anlegt, sondern dass man nicht alles auf einmal in den Speicher lädt, sondern nur das was man benötigt. Aber eben aus *einer* Datenbank. Das was Du da machst — eine grosse Datenstruktur auf einen Schlag zu de-/serialisieren — würde ich auch nicht unbedingt als Datenbank bezeichnen. Ich weiss man kann den Begriff recht weit fassen, aber Datenbank impliziert für mich in der Regel mehr Daten als man im Speicher halten kann oder möchte und mindestens einen einfachen Key/Value-Store um auf die Daten per Schlüssel zuzugreifen.
Naja wir reden ja hier von cPickle und ich habe bisher noch nicht gehört, dass man mit cPickle.load nur einen Teil der "Datenbank" laden kann oder etwa doch?!

Und wie vorhin schon gesagt, benötige ich permanent zugriff auf ALLE Daten. Was die schlussfolgerung hat, dass ich auch ALLE Daten laden muss :)
BlackJack

@Brainsucker: *Du* redest von `cPickle`. Ich wollte ja gerne mal über die Daten und die Zugriffsmuster reden. Denn vielleicht ist `cPickle` ja nicht das einzige was man verwenden kann. Und vielleicht kann man an der Datenstruktur oder dem Algoritmus auch etwas ändern.

Das Du wirklich immer Zugriff auf alle Daten brauchst, muss man jetzt wohl einfach mal glauben. Das kann ich mir eigentlich nur vorstellen wenn ständig alle Werte für eine Berechnung benötigt werden und dabei auch immer alle Werte geändert werden müssen. Denn sonst erscheint mir der alles auf einmal Laden und alles auf einmal Speichern Ansatz suboptimal.
Antworten