web-dev-qa-db-fra.com

Comment partager une seule connexion SQLite dans une application Python multi-thread

J'essaie d'écrire une application Python multi-thread dans laquelle une seule connexion SQlite est partagée entre les threads. Je suis incapable d'obtenir que cela fonctionne. La vraie application est un serveur Web cherrypy, mais le code simple suivant illustre mon problème.

Quels sont les changements à apporter pour que je puisse exécuter avec succès l'exemple de code ci-dessous?

Lorsque j'exécute ce programme avec THREAD_COUNT défini sur 1, cela fonctionne correctement et ma base de données est mise à jour comme prévu (c'est-à-dire que la lettre "X" est ajoutée à la valeur de texte dans la colonne SectorGroup).

Lorsque je l'exécute avec THREAD_COUNT défini sur une valeur supérieure à 1, tous les threads sauf 1 se terminent prématurément avec des exceptions liées à SQLite. Différents threads génèrent différentes exceptions (sans motif discernable), notamment:

OperationalError: cannot start a transaction within a transaction 

(se produit sur l'instruction UPDATE)

OperationalError: cannot commit - no transaction is active 

(se produit sur l'appel .commit ())

InterfaceError: Error binding parameter 0 - probably unsupported type. 

(se produit sur les instructions UPDATE et SELECT)

IndexError: Tuple index out of range

(Celui-ci m'a complètement dérouté, il apparaît dans l'instruction group = rows[0][0] or '', mais uniquement lorsque plusieurs threads sont en cours d'exécution)

Voici le 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

Il n'est pas prudent de partager une connexion entre les threads; à tout le moins, vous devez utiliser un verrou pour sérialiser l'accès. Lisez aussi http://docs.python.org/2/library/sqlite3.html#multithreading car les anciennes versions de SQLite posent encore plus de problèmes.

L'option check_same_thread semble délibérément sous-documentée à cet égard, voir http://bugs.python.org/issue16509 .

Vous pouvez utiliser une connexion par thread à la place, ou rechercher dans SQLAlchemy un pool de connexions (et un système de déclaration de travail et de mise en file d'attente très efficace à démarrer).

13
Martijn Pieters

J'ai rencontré un problème de threads SqLite lors de l'écriture d'un serveur WSGI simple pour le plaisir et l'apprentissage. WSGI est multi-threaded par nature lorsqu'il est exécuté sous Apache. Le code suivant semble fonctionner pour moi:

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 ())

Exemple d'utilisation:

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'])))

Un autre exemple:

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))

Programme complet utilisant cette construction téléchargeable à l’adresse: http://www.josmith.org/

N.B. Le code ci-dessus est expérimental, il peut y avoir des problèmes (fondamentaux) lorsqu’il est utilisé avec (plusieurs) demandes simultanées (par exemple dans le cadre d’un serveur WSGI). Les performances ne sont pas critiques pour mon application. La chose la plus simple aurait probablement été d’utiliser simplement MySql, mais j’aime expérimenter un peu, et l’installation zéro de SqLite m’a séduit. Si quelqu'un pense que le code ci-dessus est fondamentalement défectueux, réagissez s'il vous plaît, car mon but est d'apprendre. Sinon, j'espère que cela sera utile aux autres.

3
Jacques de Hooge

Je suppose ici, mais il semble que la raison pour laquelle vous le faites est un problème de performance. 

Les threads Python ne sont pas performants de manière significative pour ce cas d'utilisation. Utilisez plutôt des transactions sqlite, qui sont ultra rapides. 

Si vous effectuez toutes vos mises à jour dans une transaction, vous constaterez une accélération de l'ordre de grandeur. 

0
Erik Aronesty