web-dev-qa-db-de.com

So geben Sie eine einzelne SQLite-Verbindung in einer Python-Anwendung mit mehreren Threads frei

Ich versuche, eine Python-Anwendung mit mehreren Threads zu schreiben, in der eine einzelne SQlite-Verbindung von Threads gemeinsam genutzt wird. Ich kann das nicht zum Laufen bringen. Die eigentliche Anwendung ist ein cherrypy Webserver, aber der folgende einfache Code demonstriert mein Problem.

Welche Änderung oder welche Änderungen muss ich vornehmen, um den folgenden Beispielcode erfolgreich ausführen zu können?

Wenn ich dieses Programm mit THREAD_COUNT auf 1 ausführen, funktioniert es einwandfrei und meine Datenbank wird wie erwartet aktualisiert (d. H. Der Buchstabe "X" wird dem Textwert in der Spalte "SectorGroup" hinzugefügt).

Wenn ich THREAD_COUNT auf einen höheren Wert als 1 starte, werden alle Threads außer 1 vorzeitig mit SQLite-Ausnahmen beendet. Verschiedene Threads werfen unterschiedliche Ausnahmen auf (ohne erkennbares Muster), einschließlich:

OperationalError: cannot start a transaction within a transaction 

(tritt in der UPDATE-Anweisung auf)

OperationalError: cannot commit - no transaction is active 

(tritt beim Aufruf von .commit () auf)

InterfaceError: Error binding parameter 0 - probably unsupported type. 

(tritt bei den Anweisungen UPDATE und SELECT auf)

IndexError: Tuple index out of range

(Dieses hat mich völlig verwirrt, es kommt in der Anweisung group = rows[0][0] or '' vor, aber nur, wenn mehrere Threads laufen.)

Hier ist der Code:

CONNECTION = sqlite3.connect('./database/mydb', detect_types=sqlite3.PARSE_DECLTYPES, check_same_thread = False)
CONNECTION.row_factory = sqlite3.Row

def commands(start_id):

    # loop over 100 records, read the SectorGroup column, and write it back with "X" appended.
    for inv_id in range(start_id, start_id + 100):

        rows = CONNECTION.execute('SELECT SectorGroup FROM Investment WHERE InvestmentID = ?;', [inv_id]).fetchall()
        if rows:
            group = rows[0][0] or ''
            msg = '{} inv {} = {}'.format(current_thread().name, inv_id, group)
            print msg
            CONNECTION.execute('UPDATE Investment SET SectorGroup = ? WHERE InvestmentID = ?;', [group + 'X', inv_id])

        CONNECTION.commit()

if __== '__main__':

    THREAD_COUNT = 10

    for i in range(THREAD_COUNT):
        t = Thread(target=commands, args=(i*100,))
        t.start()
10
Larry Lustig

Es ist nicht sicher, eine Verbindung zwischen Threads freizugeben. Zumindest müssen Sie eine Sperre verwenden, um den Zugriff zu serialisieren. Lesen Sie auch http://docs.python.org/2/library/sqlite3.html#multithreading , da ältere SQLite-Versionen noch weitere Probleme haben.

Die check_same_thread-Option erscheint in dieser Hinsicht absichtlich unterdokumentiert, siehe http://bugs.python.org/issue16509 .

Sie können stattdessen eine Verbindung pro Thread verwenden oder bei SQLAlchemy nach einem Verbindungspool suchen (und nach einem sehr effizienten Arbeitsanweisungs- und Warteschlangensystem, das gestartet werden soll).

13
Martijn Pieters

Ich bin auf das SqLite-Threading-Problem gestoßen, als ich einen einfachen WSGI-Server zum Spaß und Lernen schrieb. WSGI ist von Natur aus Multithreading, wenn es unter Apache läuft. Der folgende Code scheint für mich zu funktionieren:

import sqlite3
import threading

class LockableCursor:
    def __init__ (self, cursor):
        self.cursor = cursor
        self.lock = threading.Lock ()

    def execute (self, arg0, arg1 = None):
        self.lock.acquire ()

        try:
            self.cursor.execute (arg1 if arg1 else arg0)

            if arg1:
                if arg0 == 'all':
                    result = self.cursor.fetchall ()
                Elif arg0 == 'one':
                    result = self.cursor.fetchone ()
        except Exception as exception:
            raise exception

        finally:
            self.lock.release ()
            if arg1:
                return result

def dictFactory (cursor, row):
    aDict = {}
    for iField, field in enumerate (cursor.description):
        aDict [field [0]] = row [iField]
    return aDict

class Db:
    def __init__ (self, app):
        self.app = app

    def connect (self):
        self.connection = sqlite3.connect (self.app.dbFileName, check_same_thread = False, isolation_level = None)  # Will create db if nonexistent
        self.connection.row_factory = dictFactory
        self.cs = LockableCursor (self.connection.cursor ())

Anwendungsbeispiel:

if not ok and self.user:    # Not logged out
    # Get role data for any later use
    userIdsRoleIds = self.cs.execute ('all', 'SELECT role_id FROM users_roles WHERE user_id == {}'.format (self.user ['id']))

    for userIdRoleId in userIdsRoleIds:
        self.userRoles.append (self.cs.execute ('one', 'SELECT name FROM roles WHERE id == {}'.format (userIdRoleId ['role_id'])))

Ein anderes Beispiel:

self.cs.execute ('CREATE TABLE users (id INTEGER PRIMARY KEY, email_address, password, token)')         
self.cs.execute ('INSERT INTO users (email_address, password) VALUES ("{}", "{}")'.format (self.app.defaultUserEmailAddress, self.app.defaultUserPassword))

# Create roles table and insert default role
self.cs.execute ('CREATE TABLE roles (id INTEGER PRIMARY KEY, name)')
self.cs.execute ('INSERT INTO roles (name) VALUES ("{}")'.format (self.app.defaultRoleName))

# Create users_roles table and assign default role to default user
self.cs.execute ('CREATE TABLE users_roles (id INTEGER PRIMARY KEY, user_id, role_id)') 

defaultUserId = self.cs.execute ('one', 'SELECT id FROM users WHERE email_address = "{}"'.format (self.app.defaultUserEmailAddress)) ['id']         
defaultRoleId = self.cs.execute ('one', 'SELECT id FROM roles WHERE name = "{}"'.format (self.app.defaultRoleName)) ['id']

self.cs.execute ('INSERT INTO users_roles (user_id, role_id) VALUES ({}, {})'.format (defaultUserId, defaultRoleId))

Komplettes Programm, das diese Konstruktion verwendet, kann heruntergeladen werden unter: http://www.josmith.org/

N.B. Der obige Code ist experimentell. Es können (grundlegende) Probleme bei der Verwendung von (vielen) gleichzeitigen Anforderungen (z. B. als Teil eines WSGI-Servers) auftreten. Die Leistung ist für meine Anwendung nicht kritisch. Die einfachste Sache wäre wahrscheinlich gewesen, nur MySql zu verwenden, aber ich experimentiere gerne ein wenig, und die Zero-Installation von SqLite hat mich angesprochen. Wenn jemand der Meinung ist, dass der obige Code grundlegend fehlerhaft ist, reagieren Sie bitte, da ich lernen möchte. Wenn nicht, hoffe ich, dass dies für andere nützlich ist.

3

Ich vermute hier, aber es scheint, dass der Grund, warum Sie dies tun, ein Performanceproblem ist. 

Python-Threads sind für diesen Anwendungsfall in keiner Weise sinnvoll. Verwenden Sie stattdessen sqlite-Transaktionen, die sehr schnell sind. 

Wenn Sie alle Ihre Aktualisierungen in einer Transaktion durchführen, wird die Geschwindigkeit um eine Größenordnung beschleunigt. 

0
Erik Aronesty