Python sqlite3 et concurrence


87

J'ai un programme Python qui utilise le module "threading". Une fois par seconde, mon programme démarre un nouveau thread qui récupère des données sur le Web et stocke ces données sur mon disque dur. Je voudrais utiliser sqlite3 pour stocker ces résultats, mais je ne peux pas le faire fonctionner. Le problème semble concerner la ligne suivante:

conn = sqlite3.connect("mydatabase.db")
  • Si je mets cette ligne de code dans chaque thread, j'obtiens un OperationalError m'indiquant que le fichier de base de données est verrouillé. Je suppose que cela signifie qu'un autre thread a ouvert mydatabase.db via une connexion sqlite3 et l'a verrouillé.
  • Si je mets cette ligne de code dans le programme principal et que je passe l'objet de connexion (conn) à chaque thread, j'obtiens un ProgrammingError, disant que les objets SQLite créés dans un thread ne peuvent être utilisés que dans ce même thread.

Auparavant, je stockais tous mes résultats dans des fichiers CSV et je n'avais aucun de ces problèmes de verrouillage de fichier. Espérons que cela sera possible avec sqlite. Des idées?


5
Je voudrais noter que les versions plus récentes de Python incluent des versions plus récentes de sqlite3 qui devraient résoudre ce problème.
Ryan Fugger

@RyanFugger savez-vous quelle est la première version qui le prend en charge? J'utilise 2.7
notbad.jpeg

@RyanFugger AFAIK il n'y a pas de version pré-construite contenant une version plus récente de SQLite3 qui a corrigé cela. Vous pouvez cependant en construire un vous-même.
shezi

Réponses:


44

Vous pouvez utiliser le modèle consommateur-producteur. Par exemple, vous pouvez créer une file d'attente partagée entre les threads. Le premier thread qui récupère les données du Web met ces données en file d'attente dans la file d'attente partagée. Un autre thread propriétaire d'une connexion à la base de données extrait les données de la file d'attente et les transmet à la base de données.


8
FWIW: Les versions ultérieures de sqlite prétendent que vous pouvez partager des connexions et des objets entre les threads (sauf les curseurs), mais j'ai trouvé le contraire dans la pratique réelle.
Richard Levasseur

Voici un exemple de ce qu'Evgeny Lazin a mentionné ci-dessus.
dugres

4
Cacher votre base de données derrière une file d'attente partagée est une très mauvaise solution à cette question car SQL en général et SQLite en particulier ont déjà des mécanismes de verrouillage intégrés, qui sont probablement beaucoup plus raffinés que tout ce que vous pouvez créer ad hoc par vous-même.
shezi

1
Vous devez lire la question, à ce moment-là, il n'y avait pas de mécanisme de verrouillage intégré. De nombreuses bases de données embarquées contemporaines ne disposent pas de ce mécanisme pour des raisons de performances (par exemple: LevelDB).
Evgeny Lazin

180

Contrairement à la croyance populaire, les nouvelles versions de sqlite3 prennent en charge l'accès à partir de plusieurs threads.

Cela peut être activé via un argument de mot clé facultatif check_same_thread:

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

4
J'ai rencontré des exceptions imprévisibles et même des plantages de Python avec cette option (Python 2.7 sur Windows 32).
reclosedev

4
Selon la documentation , en mode multi-thread, aucune connexion à une base de données ne peut être utilisée dans plusieurs threads. Il y a aussi un mode sérialisé
Casebash

1
Qu'à cela ne tienne, je viens de le trouver: http://sqlite.org/compile.html#threadsafe
Medeiros

1
@FrEaKmAn, désolé, c'était il y a longtemps, pas non plus: mémoire: base de données. Après cela, je n'ai pas partagé de connexion sqlite dans plusieurs threads.
reclosedev

2
@FrEaKmAn, j'ai rencontré cela, avec le core-dumping du processus python sur l'accès multi-thread. Le comportement était imprévisible et aucune exception n'a été enregistrée. Si je me souviens bien, c'était vrai pour les lectures et les écritures. C'est la seule chose que j'ai vu planter python jusqu'à présent: D. Je n'ai pas essayé cela avec sqlite compilé en mode threadsafe, mais à l'époque, je n'avais pas la liberté de recompiler le sqlite par défaut du système. J'ai fini par faire quelque chose qui ressemble à ce qu'Eric a suggéré et
j'ai

17

Ce qui suit trouvé sur mail.python.org.pipermail.1239789

J'ai trouvé la solution. Je ne sais pas pourquoi la documentation python n'a pas un seul mot sur cette option. Nous devons donc ajouter un nouvel argument mot-clé à la fonction de connexion et nous pourrons en créer des curseurs dans un thread différent. Alors utilisez:

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

fonctionne parfaitement pour moi. Bien sûr, à partir de maintenant, je dois m'occuper d'un accès multithreading sécurisé à la base de données. Quoi qu'il en soit merci à tous d'avoir essayé d'aider.


(Avec le GIL, il n'y a vraiment pas grand-chose sur le chemin d'un véritable accès multithread à la base de données de toute façon que j'ai vu)
Erik Aronesty

AVERTISSEMENT : La documentation Python a ceci à dire à propos de l' check_same_threadoption: "Lors de l'utilisation de plusieurs threads avec la même connexion, les opérations d'écriture doivent être sérialisées par l'utilisateur pour éviter la corruption des données." Donc oui, vous pouvez utiliser SQLite avec plusieurs threads tant que votre code garantit qu'un seul thread peut écrire dans la base de données à un moment donné. Sinon, vous risquez de corrompre votre base de données.
Ajedi32 le

14

Passez au multitraitement . C'est beaucoup mieux, évolue bien, peut aller au-delà de l'utilisation de plusieurs cœurs en utilisant plusieurs processeurs, et l'interface est la même que l'utilisation du module de threading python.

Ou, comme Ali l'a suggéré, utilisez simplement le mécanisme de pool de threads de SQLAlchemy . Il gérera tout automatiquement pour vous et possède de nombreuses fonctionnalités supplémentaires, pour n'en citer que quelques-unes:

  1. SQLAlchemy comprend des dialectes pour SQLite, Postgres, MySQL, Oracle, MS-SQL, Firebird, MaxDB, MS Access, Sybase et Informix; IBM a également publié un pilote DB2. Vous n'avez donc pas à réécrire votre application si vous décidez de vous éloigner de SQLite.
  2. Le système Unit Of Work, une partie centrale de l'Object Relational Mapper (ORM) de SQLAlchemy, organise les opérations de création / insertion / mise à jour / suppression en attente dans des files d'attente et les vide toutes en un seul lot. Pour ce faire, il effectue un "tri de dépendances" topologique de tous les éléments modifiés dans la file d'attente afin d'honorer les contraintes de clé étrangère, et regroupe les instructions redondantes où elles peuvent parfois être regroupées encore plus. Cela produit l'efficacité maximale et la sécurité des transactions, et minimise les risques de blocages.

12

Vous ne devriez pas du tout utiliser de threads pour cela. C'est une tâche triviale pour Twisted et qui vous mènerait probablement beaucoup plus loin de toute façon.

N'utilisez qu'un seul thread et que l'achèvement de la demande déclenche un événement pour effectuer l'écriture.

twisted s'occupera de la planification, des rappels, etc ... pour vous. Il vous remettra le résultat complet sous forme de chaîne, ou vous pouvez l'exécuter via un processeur de flux (j'ai une API Twitter et une API Friendfeed qui déclenchent les événements aux appelants lorsque les résultats sont toujours en cours de téléchargement).

En fonction de ce que vous faites avec vos données, vous pouvez simplement vider le résultat complet dans sqlite une fois terminé, le cuire et le vider, ou le cuire pendant sa lecture et le vider à la fin.

J'ai une application très simple qui fait quelque chose de proche de ce que vous voulez sur github. Je l'appelle pfetch ( extraction parallèle). Il saisit diverses pages selon un calendrier, diffuse les résultats dans un fichier et exécute éventuellement un script une fois chacune terminée. Il fait également des trucs fantaisistes comme les GET conditionnels, mais pourrait toujours être une bonne base pour tout ce que vous faites.


7

Ou si vous êtes paresseux, comme moi, vous pouvez utiliser SQLAlchemy . Il gérera le threading pour vous (en utilisant le thread local et un pool de connexions ) et la façon dont il le fait est même configurable .

Pour un bonus supplémentaire, si / quand vous réalisez / décidez que l'utilisation de Sqlite pour une application simultanée va être un désastre, vous n'aurez pas à changer votre code pour utiliser MySQL, ou Postgres, ou quoi que ce soit d'autre. Vous pouvez simplement basculer.


1
Pourquoi ne spécifie-t-il pas la version Python n'importe où sur le site officiel?
Afficher le nom du

3

Vous devez utiliser session.close()après chaque transaction vers la base de données afin d'utiliser le même curseur dans le même thread sans utiliser le même curseur dans les multi-threads qui provoquent cette erreur.



0

J'aime la réponse d'Evgeny - Les files d'attente sont généralement le meilleur moyen d'implémenter la communication inter-thread. Pour être complet, voici quelques autres options:

  • Fermez la connexion à la base de données lorsque les threads générés ont fini de l'utiliser. Cela réglerait vos problèmes OperationalError, mais l'ouverture et la fermeture de connexions comme celle-ci est généralement un non-non, en raison de la surcharge de performances.
  • N'utilisez pas de threads enfants. Si la tâche une fois par seconde est raisonnablement légère, vous pouvez vous en sortir avec la récupération et le stockage, puis dormir jusqu'au bon moment. Cela n'est pas souhaitable car les opérations de récupération et de stockage peuvent prendre> 1 seconde et vous perdez le bénéfice des ressources multiplexées dont vous disposez avec une approche multi-thread.

0

Vous devez concevoir la simultanéité de votre programme. SQLite a des limitations claires et vous devez y obéir, voir la FAQ (également la question suivante).


0

Scrapy semble être une réponse potentielle à ma question. Sa page d'accueil décrit ma tâche exacte. (Bien que je ne sois pas encore sûr de la stabilité du code.)


0

Je jetterais un œil au module Y_serial Python pour la persistance des données: http://yserial.sourceforge.net

qui gère les problèmes de blocage entourant une seule base de données SQLite. Si la demande de concurrence devient lourde, on peut facilement configurer la classe Farm de nombreuses bases de données pour diffuser la charge sur le temps stochastique.

J'espère que cela aidera votre projet ... il devrait être assez simple à mettre en œuvre en 10 minutes.


0

Je n'ai trouvé aucun point de repère dans aucune des réponses ci-dessus, alors j'ai écrit un test pour tout comparer.

J'ai essayé 3 approches

  1. Lecture et écriture séquentielles à partir de la base de données SQLite
  2. Utilisation d'un ThreadPoolExecutor pour lire / écrire
  3. Utilisation d'un ProcessPoolExecutor pour lire / écrire

Les résultats et les points à retenir du benchmark sont les suivants

  1. Les lectures séquentielles / écritures séquentielles fonctionnent le mieux
  2. Si vous devez traiter en parallèle, utilisez ProcessPoolExecutor pour lire en parallèle
  3. N'effectuez aucune écriture en utilisant ThreadPoolExecutor ou en utilisant ProcessPoolExecutor car vous rencontrerez des erreurs de base de données verrouillées et vous devrez réessayer d'insérer le bloc à nouveau

Vous pouvez trouver le code et la solution complète pour les benchmarks dans ma réponse SO ICI J'espère que cela aide!


-1

La raison la plus probable pour laquelle vous obtenez des erreurs avec des bases de données verrouillées est que vous devez émettre

conn.commit()

après avoir terminé une opération de base de données. Si vous ne le faites pas, votre base de données sera verrouillée en écriture et le restera. Les autres threads en attente d'écriture expireront après un certain temps (la valeur par défaut est définie sur 5 secondes, voir http://docs.python.org/2/library/sqlite3.html#sqlite3.connect pour plus de détails à ce sujet) .

Voici un exemple d'insertion correcte et concurrente:

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

Si vous aimez SQLite, ou avez d'autres outils qui fonctionnent avec les bases de données SQLite, ou souhaitez remplacer les fichiers CSV par des fichiers SQLite db, ou devez faire quelque chose de rare comme IPC inter-plateforme, alors SQLite est un excellent outil et très approprié à cet effet. Ne vous laissez pas contraindre à utiliser une solution différente si cela ne vous convient pas!

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.