Python sqlite3 und Parallelität

85

Ich habe ein Python-Programm, das das "Threading" -Modul verwendet. Einmal pro Sekunde startet mein Programm einen neuen Thread, der einige Daten aus dem Web abruft und diese Daten auf meiner Festplatte speichert. Ich möchte sqlite3 verwenden, um diese Ergebnisse zu speichern, aber ich kann es nicht zum Laufen bringen. Das Problem scheint in der folgenden Zeile zu liegen:

conn = sqlite3.connect("mydatabase.db")
  • Wenn ich diese Codezeile in jeden Thread einfüge, wird ein OperationalError angezeigt, der mir mitteilt, dass die Datenbankdatei gesperrt ist. Ich denke, dies bedeutet, dass ein anderer Thread mydatabase.db über eine sqlite3-Verbindung geöffnet und gesperrt hat.
  • Wenn ich diese Codezeile in das Hauptprogramm einfüge und das Verbindungsobjekt (conn) an jeden Thread übergebe, erhalte ich einen Programmierfehler, der besagt, dass in einem Thread erstellte SQLite-Objekte nur in demselben Thread verwendet werden können.

Zuvor habe ich alle meine Ergebnisse in CSV-Dateien gespeichert und hatte keine dieser Probleme beim Sperren von Dateien. Hoffentlich wird dies mit SQLite möglich sein. Irgendwelche Ideen?

RexE
quelle
5
Ich möchte darauf hinweisen, dass neuere Versionen von Python neuere Versionen von sqlite3 enthalten, die dieses Problem beheben sollten.
Ryan Fugger
@ RyanFugger wissen Sie, was die früheste Version ist, die dies unterstützt? Ich benutze 2.7
notbad.jpeg
@ RyanFugger AFAIK Es gibt keine vorgefertigte Version, die eine neuere Version von SQLite3 enthält, die dies behoben hat. Sie können jedoch selbst eine bauen.
Shezi

Antworten:

42

Sie können das Consumer-Producer-Muster verwenden. Beispielsweise können Sie eine Warteschlange erstellen, die von Threads gemeinsam genutzt wird. Der erste Thread, der Daten aus dem Web abruft, stellt diese Daten in die gemeinsam genutzte Warteschlange. Ein anderer Thread, der eine Datenbankverbindung besitzt, entfernt Daten aus der Warteschlange und leitet sie an die Datenbank weiter.

Evgeny Lazin
quelle
8
FWIW: Spätere Versionen von SQLite behaupten, Sie könnten Verbindungen und Objekte über Threads hinweg teilen (außer Cursor), aber ich habe in der Praxis etwas anderes festgestellt.
Richard Levasseur
Hier ist ein Beispiel für das, was Evgeny Lazin oben erwähnt hat.
Dugres
4
Das Ausblenden Ihrer Datenbank hinter einer gemeinsam genutzten Warteschlange ist eine wirklich schlechte Lösung für diese Frage, da SQL im Allgemeinen und SQLite im Besonderen bereits über integrierte Sperrmechanismen verfügen, die wahrscheinlich viel ausgefeilter sind als alles, was Sie selbst ad-hoc erstellen können.
Shezi
1
Sie müssen die Frage lesen, in diesem Moment gab es keine eingebauten Verriegelungsmechanismen. Vielen modernen eingebetteten Datenbanken fehlt dieser Mechanismus aus Leistungsgründen (zum Beispiel: LevelDB).
Evgeny Lazin
177

Entgegen der landläufigen Meinung, neuere Versionen von sqlite3 tun unterstützen den Zugriff von mehreren Threads.

Dies kann über ein optionales Schlüsselwortargument aktiviert werden check_same_thread:

sqlite.connect(":memory:", check_same_thread=False)
Jeremiah Rose
quelle
4
Ich habe unvorhersehbare Ausnahmen festgestellt und sogar Python stürzt mit dieser Option ab (Python 2.7 unter Windows 32).
reclosedev
4
Laut den Dokumenten kann im Multithread-Modus keine einzelne Datenbankverbindung in mehreren Threads verwendet werden. Es gibt auch einen serialisierten Modus
Casebash
1
Egal, ich habe es gerade gefunden: http://sqlite.org/compile.html#threadsafe
Medeiros
1
@FrEaKmAn, sorry, es ist lange her, auch nicht: memory: database. Danach habe ich die SQLite-Verbindung nicht mehr in mehreren Threads geteilt.
reclosedev
2
@FrEaKmAn, ich habe dies beim Core-Dumping des Python-Prozesses beim Multithread-Zugriff festgestellt. Das Verhalten war unvorhersehbar und es wurde keine Ausnahme protokolliert. Wenn ich mich richtig erinnere, galt dies sowohl für Lese- als auch für Schreibvorgänge. Dies ist die einzige Sache, die ich bisher tatsächlich als Absturz von Python gesehen habe: D. Ich habe dies nicht mit SQLite versucht, das im threadsicheren Modus kompiliert wurde, aber zu diesem Zeitpunkt hatte ich nicht die Freiheit, das Standard-SQLite des Systems neu zu kompilieren.
Am
17

Folgendes auf mail.python.org.pipermail.1239789 gefunden

Ich habe die Lösung gefunden. Ich weiß nicht, warum die Python-Dokumentation kein einziges Wort zu dieser Option enthält. Wir müssen also der Verbindungsfunktion ein neues Schlüsselwortargument hinzufügen, und wir können daraus Cursor in verschiedenen Threads erstellen. Verwenden Sie also:

sqlite.connect(":memory:", check_same_thread = False)

funktioniert perfekt für mich. Natürlich muss ich mich von nun an um einen sicheren Multithreading-Zugriff auf die Datenbank kümmern. Trotzdem vielen Dank für den Versuch zu helfen.

Robert Krolik
quelle
(Mit der GIL gibt es wirklich nicht viel im Wege eines echten Multithread-Zugriffs auf die Datenbank, den ich gesehen habe)
Erik Aronesty
14

Wechseln Sie zu Multiprocessing . Es ist viel besser, lässt sich gut skalieren, kann über die Verwendung mehrerer Kerne hinausgehen, indem mehrere CPUs verwendet werden, und die Schnittstelle entspricht der Verwendung des Python-Threading-Moduls.

Oder verwenden Sie, wie Ali vorgeschlagen hat, einfach den Thread-Pooling-Mechanismus von SQLAlchemy . Es erledigt alles automatisch für Sie und verfügt über viele zusätzliche Funktionen, um nur einige zu nennen:

  1. SQLAlchemy enthält Dialekte für SQLite, Postgres, MySQL, Oracle, MS-SQL, Firebird, MaxDB, MS Access, Sybase und Informix. IBM hat auch einen DB2-Treiber veröffentlicht. Sie müssen Ihre Anwendung also nicht neu schreiben, wenn Sie sich von SQLite entfernen möchten.
  2. Das Unit Of Work-System, ein zentraler Bestandteil des Object Relational Mapper (ORM) von SQLAlchemy, organisiert ausstehende Erstellungs- / Einfüge- / Aktualisierungs- / Löschvorgänge in Warteschlangen und löscht sie alle in einem Stapel. Um dies zu erreichen, führt es eine topologische "Abhängigkeitssortierung" aller geänderten Elemente in der Warteschlange durch, um Fremdschlüsseleinschränkungen zu berücksichtigen, und gruppiert redundante Anweisungen, in denen sie manchmal noch weiter gestapelt werden können. Dies sorgt für maximale Effizienz und Transaktionssicherheit und minimiert das Risiko von Deadlocks.
nosklo
quelle
12

Sie sollten dafür überhaupt keine Threads verwenden. Dies ist eine triviale Aufgabe für Twisted und würde Sie wahrscheinlich sowieso erheblich weiter bringen.

Verwenden Sie nur einen Thread und lassen Sie nach Abschluss der Anforderung ein Ereignis auslösen, um den Schreibvorgang durchzuführen.

Twisted kümmert sich um die Planung, Rückrufe usw. für Sie. Sie erhalten das gesamte Ergebnis als Zeichenfolge oder können es über einen Stream-Prozessor ausführen (ich habe eine Twitter-API und eine Friendfeed-API , die beide Ereignisse an Anrufer auslösen , da die Ergebnisse noch heruntergeladen werden).

Je nachdem, was Sie mit Ihren Daten tun, können Sie das vollständige Ergebnis einfach in SQLite speichern, es kochen und sichern oder es während des Lesens kochen und am Ende sichern.

Ich habe eine sehr einfache Anwendung, die etwas in der Nähe von dem macht, was Sie auf Github wollen. Ich nenne es pfetch (paralleles Abrufen). Es erfasst verschiedene Seiten nach einem Zeitplan, überträgt die Ergebnisse in eine Datei und führt optional ein Skript aus, wenn jede Seite erfolgreich abgeschlossen wurde. Es macht auch einige ausgefallene Dinge wie bedingte GETs, könnte aber dennoch eine gute Basis für alles sein, was Sie tun.

Dustin
quelle
7

Oder wenn Sie faul sind, wie ich, können Sie SQLAlchemy verwenden . Es übernimmt das Threading für Sie (unter Verwendung des lokalen Threads und einiger Verbindungspools ) und die Art und Weise, wie es ausgeführt wird, ist sogar konfigurierbar .

Wenn Sie feststellen, dass die Verwendung von Sqlite für eine gleichzeitige Anwendung eine Katastrophe darstellt, müssen Sie Ihren Code nicht ändern, um MySQL, Postgres oder etwas anderes zu verwenden. Sie können einfach umschalten.

Ali Afshar
quelle
1
Warum wird die Python-Version nirgendwo auf der offiziellen Website angegeben?
Anzeigename
2

Sie müssen session.close()nach jeder Transaktion in die Datenbank verwenden, um denselben Cursor im selben Thread zu verwenden, und nicht denselben Cursor in Multithreads, die diesen Fehler verursachen.

Hazem Khaled
quelle
1

Verwenden Sie threading.Lock ()

Alexandr
quelle
Bitte geben Sie an, was der folgende Code bewirkt und wo er verwendet werden soll.
Ali Akhtari
0

Ich mag Evgenys Antwort: Warteschlangen sind im Allgemeinen der beste Weg, um die Kommunikation zwischen Threads zu implementieren. Der Vollständigkeit halber sind hier einige andere Optionen:

  • Schließen Sie die DB-Verbindung, wenn die erzeugten Threads sie nicht mehr verwenden. Dies würde Ihre Probleme beheben OperationalError, aber das Öffnen und Schließen solcher Verbindungen ist aufgrund des Leistungsaufwands im Allgemeinen ein Nein-Nein.
  • Verwenden Sie keine untergeordneten Threads. Wenn die Aufgabe einmal pro Sekunde relativ leicht ist, können Sie mit dem Abrufen und Speichern davonkommen und dann bis zum richtigen Moment schlafen. Dies ist unerwünscht, da Abruf- und Speichervorgänge> 1 Sekunde dauern können und Sie den Vorteil von Multiplex-Ressourcen verlieren, die Sie mit einem Multithread-Ansatz haben.
James Brady
quelle
0

Sie müssen die Parallelität für Ihr Programm entwerfen. SQLite hat klare Einschränkungen und Sie müssen diese beachten, siehe FAQ (auch die folgende Frage).

iny
quelle
0

Scrapy scheint eine mögliche Antwort auf meine Frage zu sein. Die Homepage beschreibt meine genaue Aufgabe. (Obwohl ich nicht sicher bin, wie stabil der Code noch ist.)

RexE
quelle
0

Ich würde mir das y_serial Python-Modul für die Datenpersistenz ansehen: http://yserial.sourceforge.net

Hiermit werden Deadlock-Probleme im Zusammenhang mit einer einzelnen SQLite-Datenbank behoben. Wenn die Nachfrage nach Parallelität stark wird, kann die Klassenfarm vieler Datenbanken leicht eingerichtet werden, um die Last über die stochastische Zeit zu verteilen.

Ich hoffe, dies hilft Ihrem Projekt ... es sollte einfach genug sein, um es in 10 Minuten zu implementieren.

Code43
quelle
0

Ich konnte in keiner der obigen Antworten Benchmarks finden, also schrieb ich einen Test, um alles zu bewerten.

Ich habe 3 Ansätze ausprobiert

  1. Sequentielles Lesen und Schreiben aus der SQLite-Datenbank
  2. Verwenden eines ThreadPoolExecutors zum Lesen / Schreiben
  3. Verwenden eines ProcessPoolExecutors zum Lesen / Schreiben

Die Ergebnisse und Erkenntnisse aus der Benchmark sind wie folgt

  1. Sequentielle Lese- / Schreibvorgänge funktionieren am besten
  2. Wenn Sie parallel verarbeiten müssen, verwenden Sie den ProcessPoolExecutor, um parallel zu lesen
  3. Führen Sie keine Schreibvorgänge mit dem ThreadPoolExecutor oder dem ProcessPoolExecutor durch, da sonst datenbankgesperrte Fehler auftreten und Sie erneut versuchen müssen, den Block einzufügen

Den Code und die Komplettlösung für die Benchmarks finden Sie in meiner SO-Antwort HIER. Hoffe, das hilft!

PirateApp
quelle
-1

Der wahrscheinlichste Grund für Fehler bei gesperrten Datenbanken ist, dass Sie Probleme haben müssen

conn.commit()

nach Abschluss einer Datenbankoperation. Wenn Sie dies nicht tun, wird Ihre Datenbank schreibgeschützt und bleibt dies auch. Für die anderen Threads, die auf das Schreiben warten, tritt nach einiger Zeit eine Zeitüberschreitung auf (die Standardeinstellung ist 5 Sekunden, siehe http://docs.python.org/2/library/sqlite3.html#sqlite3.connect für Details dazu). .

Ein Beispiel für eine korrekte und gleichzeitige Einfügung wäre:

import threading, sqlite3
class InsertionThread(threading.Thread):

    def __init__(self, number):
        super(InsertionThread, self).__init__()
        self.number = number

    def run(self):
        conn = sqlite3.connect('yourdb.db', timeout=5)
        conn.execute('CREATE TABLE IF NOT EXISTS threadcount (threadnum, count);')
        conn.commit()

        for i in range(1000):
            conn.execute("INSERT INTO threadcount VALUES (?, ?);", (self.number, i))
            conn.commit()

# create as many of these as you wish
# but be careful to set the timeout value appropriately: thread switching in
# python takes some time
for i in range(2):
    t = InsertionThread(i)
    t.start()

Wenn Sie SQLite mögen oder andere Tools haben, die mit SQLite-Datenbanken arbeiten, oder CSV-Dateien durch SQLite-Datenbankdateien ersetzen möchten oder etwas Seltenes wie plattformübergreifendes IPC ausführen müssen, ist SQLite ein großartiges Tool und für diesen Zweck sehr geeignet. Lassen Sie sich nicht unter Druck setzen, eine andere Lösung zu verwenden, wenn sie sich nicht richtig anfühlt!

shezi
quelle