Python sqlite3 e concorrenza


87

Ho un programma Python che utilizza il modulo "threading". Una volta al secondo, il mio programma avvia un nuovo thread che recupera alcuni dati dal web e li memorizza sul mio disco rigido. Vorrei utilizzare sqlite3 per memorizzare questi risultati, ma non riesco a farlo funzionare. Il problema sembra riguardare la seguente riga:

conn = sqlite3.connect("mydatabase.db")
  • Se inserisco questa riga di codice all'interno di ogni thread, ottengo un OperationalError che mi dice che il file del database è bloccato. Immagino che questo significhi che un altro thread ha mydatabase.db aperto tramite una connessione sqlite3 e lo ha bloccato.
  • Se inserisco questa riga di codice nel programma principale e passo l'oggetto di connessione (conn) a ciascun thread, ottengo un'eccezione ProgrammingError, che dice che gli oggetti SQLite creati in un thread possono essere utilizzati solo in quello stesso thread.

In precedenza memorizzavo tutti i miei risultati in file CSV e non avevo nessuno di questi problemi di blocco dei file. Si spera che questo sarà possibile con sqlite. Qualche idea?


5
Vorrei sottolineare che le versioni più recenti di Python includono versioni più recenti di sqlite3 che dovrebbero risolvere questo problema.
Ryan Fugger

@ RyanFugger sai qual è la prima versione che lo supporta? Sto usando 2.7
notbad.jpeg

@RyanFugger AFAIK non esiste una versione precostruita che contenga una versione più recente di SQLite3 che ha risolto il problema. Puoi costruirne uno tu stesso, però.
Shezi

Risposte:


44

Puoi utilizzare il pattern produttore-consumatore. Ad esempio, puoi creare una coda condivisa tra i thread. Il primo thread che recupera i dati dal Web accoda questi dati nella coda condivisa. Un altro thread che possiede la connessione al database rimuove i dati dalla coda e li passa al database.


8
FWIW: le versioni successive di sqlite affermano che è possibile condividere connessioni e oggetti tra thread (eccetto i cursori), ma ho trovato il contrario nella pratica.
Richard Levasseur

Ecco un esempio di ciò che Evgeny Lazin ha menzionato sopra.
dugres

4
Nascondere il database dietro una coda condivisa è una pessima soluzione a questa domanda perché SQL in generale e SQLite in particolare hanno già meccanismi di blocco incorporati, che sono probabilmente molto più raffinati di qualsiasi cosa tu possa costruire ad-hoc da solo.
Shezi

1
Devi leggere la domanda, in quel momento non c'erano meccanismi di blocco integrati. Molti database incorporati contemporanei non dispongono di questo meccanismo per motivi di prestazioni (ad esempio: LevelDB).
Evgeny Lazin

180

Contrariamente alla credenza popolare, le nuove versioni di sqlite3 fanno accesso sostegno da più thread.

Questo può essere abilitato tramite l'argomento della parola chiave opzionale check_same_thread:

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

4
Ho riscontrato eccezioni imprevedibili e persino arresti anomali di Python con questa opzione (Python 2.7 su Windows 32).
reclosedev

4
Secondo la documentazione , in modalità multi-thread non è possibile utilizzare una singola connessione al database in più thread. C'è anche una modalità serializzata
Casebash

1
Non importa, l'ho appena trovato: http://sqlite.org/compile.html#threadsafe
Medeiros,

1
@FrEaKmAn, scusa, è stato molto tempo fa, anche no: memory: database. Successivamente non ho condiviso la connessione sqlite in più thread.
reclosedev

2
@FrEaKmAn, ho riscontrato questo problema, con il core-dumping del processo Python sull'accesso multi-thread. Il comportamento era imprevedibile e non è stata registrata alcuna eccezione. Se ricordo bene, questo era vero sia per le letture che per le scritture. Questa è l'unica cosa che ho visto finora in crash Python: D. Non l'ho provato con sqlite compilato in modalità threadsafe, ma al momento non avevo la libertà di ricompilare sqlite predefinito del sistema. Ho finito per fare qualcosa di simile a quanto suggerito da Eric e disabilitato la compatibilità dei thread
verboze

17

Quanto segue trovato su mail.python.org.pipermail.1239789

ho trovato la soluzione. Non so perché la documentazione di Python non abbia una sola parola su questa opzione. Quindi dobbiamo aggiungere un nuovo argomento parola chiave alla funzione di connessione e saremo in grado di creare cursori da esso in thread diversi. Quindi usa:

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

funziona perfettamente per me. Ovviamente d'ora in poi devo occuparmi dell'accesso sicuro al multithreading al db. Comunque grazie a tutti per aver cercato di aiutare.


(Con il GIL, non c'è davvero molto in termini di vero accesso multithread al db comunque che
io

ATTENZIONE : La documentazione di Python ha questo da dire check_same_threadsull'opzione: "Quando si utilizzano più thread con la stessa connessione, le operazioni di scrittura dovrebbero essere serializzate dall'utente per evitare il danneggiamento dei dati." Quindi sì, puoi usare SQLite con più thread purché il tuo codice assicuri che un solo thread possa scrivere nel database in un dato momento. In caso contrario, potresti danneggiare il tuo database.
Ajedi32

14

Passa al multiprocessing . È molto meglio, scala bene, può andare oltre l'uso di più core utilizzando più CPU e l'interfaccia è la stessa del modulo di threading python.

Oppure, come suggerito da Ali, usa semplicemente il meccanismo di pooling dei thread di SQLAlchemy . Gestirà tutto automaticamente per te e ha molte funzionalità extra, solo per citarne alcune:

  1. SQLAlchemy include dialetti per SQLite, Postgres, MySQL, Oracle, MS-SQL, Firebird, MaxDB, MS Access, Sybase e Informix; IBM ha anche rilasciato un driver DB2. Quindi non devi riscrivere la tua applicazione se decidi di allontanarti da SQLite.
  2. Il sistema Unit Of Work, una parte centrale dell'Object Relational Mapper (ORM) di SQLAlchemy, organizza le operazioni di creazione / inserimento / aggiornamento / eliminazione in sospeso in code e le scarica tutte in un unico batch. Per ottenere ciò, esegue un "ordinamento delle dipendenze" topologico di tutti gli elementi modificati nella coda in modo da rispettare i vincoli di chiave esterna e raggruppa le istruzioni ridondanti dove a volte possono essere raggruppate ulteriormente. Ciò produce la massima efficienza e sicurezza delle transazioni e riduce al minimo le possibilità di deadlock.

12

Non dovresti assolutamente usare i thread per questo. Questo è un compito banale per i contorti e probabilmente ti porterebbe comunque molto più lontano.

Usa un solo thread e fai in modo che il completamento della richiesta attivi un evento per eseguire la scrittura.

twisted si prenderà cura della programmazione, dei callback, ecc ... per te. Ti consegnerà l'intero risultato come una stringa, oppure puoi eseguirlo tramite un processore di flusso (ho un'API Twitter e un'API Friendfeed che attivano entrambi gli eventi ai chiamanti mentre i risultati sono ancora in fase di download).

A seconda di cosa stai facendo con i tuoi dati, potresti semplicemente scaricare il risultato completo in sqlite non appena è completo, cucinarlo e scaricarlo o cucinarlo mentre viene letto e scaricarlo alla fine.

Ho un'applicazione molto semplice che fa qualcosa di simile a quello che vuoi su GitHub. Lo chiamo pfetch (recupero parallelo). Cattura varie pagine su una pianificazione, trasmette i risultati a un file e, facoltativamente, esegue uno script al completamento con successo di ciascuna di esse. Fa anche cose fantasiose come i GET condizionali, ma potrebbe comunque essere una buona base per qualunque cosa tu stia facendo.


7

O se sei pigro, come me, puoi usare SQLAlchemy . Gestirà il threading per te ( usando il thread locale e un po 'di pool di connessioni ) e il modo in cui lo fa è persino configurabile .

Per un ulteriore bonus, se / quando realizzi / decidi che l'utilizzo di Sqlite per qualsiasi applicazione simultanea sarà un disastro, non dovrai cambiare il tuo codice per usare MySQL, Postgres o qualsiasi altra cosa. Puoi semplicemente passare.


1
Perché non specifica la versione di Python da nessuna parte sul sito ufficiale?
Nome visualizzato

3

È necessario utilizzare session.close()dopo ogni transazione al database per utilizzare lo stesso cursore nello stesso thread senza utilizzare lo stesso cursore in multi-thread che causano questo errore.



0

Mi piace la risposta di Evgeny: le code sono generalmente il modo migliore per implementare la comunicazione tra thread. Per completezza, ecco alcune altre opzioni:

  • Chiudere la connessione DB quando i thread generati hanno finito di usarla. Ciò risolverebbe il problema OperationalError, ma l'apertura e la chiusura di connessioni come questa è generalmente un No-No, a causa del sovraccarico delle prestazioni.
  • Non utilizzare thread figlio. Se l'attività una volta al secondo è ragionevolmente leggera, potresti cavartela con il recupero e l'archiviazione, quindi dormire fino al momento giusto. Ciò è indesiderabile in quanto le operazioni di recupero e archiviazione potrebbero richiedere> 1 secondo e si perde il vantaggio delle risorse multiplex che si hanno con un approccio multi-thread.

0

Devi progettare la concorrenza per il tuo programma. SQLite ha dei limiti chiari e devi obbedirli, vedi le FAQ (anche la seguente domanda).


0

Scrapy sembra una potenziale risposta alla mia domanda. La sua home page descrive il mio compito esatto. (Anche se non sono ancora sicuro di quanto sia stabile il codice.)


0

Vorrei dare un'occhiata al modulo Python y_serial per la persistenza dei dati: http://yserial.sourceforge.net

che gestisce i problemi di deadlock che circondano un singolo database SQLite. Se la domanda sulla concorrenza diventa pesante, è possibile impostare facilmente la farm di classi di molti database per diffondere il carico nel tempo stocastico.

Spero che questo aiuti il ​​tuo progetto ... dovrebbe essere abbastanza semplice da implementare in 10 minuti.


0

Non sono riuscito a trovare alcun benchmark in nessuna delle risposte precedenti, quindi ho scritto un test per confrontare tutto.

Ho provato 3 approcci

  1. Lettura e scrittura sequenziale dal database SQLite
  2. Utilizzo di un ThreadPoolExecutor per leggere / scrivere
  3. Utilizzando un ProcessPoolExecutor per leggere / scrivere

I risultati e le conclusioni del benchmark sono i seguenti

  1. Le letture sequenziali / scritture sequenziali funzionano al meglio
  2. Se è necessario elaborare in parallelo, utilizzare ProcessPoolExecutor per leggere in parallelo
  3. Non eseguire alcuna scrittura né utilizzando ThreadPoolExecutor né utilizzando ProcessPoolExecutor poiché si verificheranno errori di blocco del database e sarà necessario riprovare a inserire nuovamente il blocco

Puoi trovare il codice e la soluzione completa per i benchmark nella mia risposta SO QUI Spero che aiuti!


-1

Il motivo più probabile per cui ricevi errori con database bloccati è che devi emettere

conn.commit()

dopo aver terminato un'operazione di database. Se non lo fai, il tuo database sarà bloccato in scrittura e rimarrà tale. Gli altri thread in attesa di scrittura scadranno dopo un po 'di tempo (l'impostazione predefinita è 5 secondi, vedere http://docs.python.org/2/library/sqlite3.html#sqlite3.connect per i dettagli) .

Un esempio di inserimento corretto e simultaneo sarebbe questo:

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

Se ti piace SQLite, o hai altri strumenti che funzionano con i database SQLite, o desideri sostituire i file CSV con file SQLite db, o devi fare qualcosa di raro come IPC inter-piattaforma, allora SQLite è un ottimo strumento e molto adatto allo scopo. Non lasciarti spingere nell'usare una soluzione diversa se non ti sembra giusto!

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.