Python sqlite3 та паралельність


87

У мене є програма Python, яка використовує модуль "потоків". Раз на секунду моя програма запускає новий потік, який отримує деякі дані з Інтернету та зберігає ці дані на моєму жорсткому диску. Я хотів би використовувати sqlite3 для зберігання цих результатів, але я не можу змусити його працювати. Проблема, схоже, стосується такого рядка:

conn = sqlite3.connect("mydatabase.db")
  • Якщо я розміщу цей рядок коду всередині кожного потоку, я отримую OperationalError, який повідомляє мені, що файл бази даних заблокований. Я думаю, це означає, що інший потік відкрив mydatabase.db через з'єднання sqlite3 і заблокував його.
  • Якщо я поміщу цей рядок коду в основну програму і передаю об'єкт з'єднання (conn) кожному потоку, я отримую ProgrammingError, кажучи, що об'єкти SQLite, створені в потоці, можуть використовуватися лише в цьому ж потоці.

Раніше я зберігав усі свої результати у файлах CSV і не мав жодної з цих проблем із блокуванням файлів. Сподіваємось, це буде можливо з sqlite. Будь-які ідеї?


5
Я хотів би зазначити, що новіші версії Python включають новіші версії sqlite3, які повинні вирішити цю проблему.
Райан Фуггер,

@RyanFugger чи знаєш ти, яка найраніша версія, яка підтримує це? Я використовую 2.7
notbad.jpeg

@RyanFugger AFAIK немає жодної заздалегідь створеної версії, яка б містила новішу версію SQLite3, яка б це виправила. Однак ви можете створити його самостійно.
shezi

Відповіді:


44

Ви можете використовувати шаблон споживача-виробника. Наприклад, ви можете створити чергу, спільну для потоків. Перший потік, який отримує дані з Інтернету, додає ці дані до спільної черги. Інший потік, який володіє підключенням до бази даних, виводить із черги дані та передає їх до бази даних.


8
FWIW: Пізніші версії sqlite стверджують, що ви можете ділитися з'єднаннями та об'єктами між потоками (крім курсорів), але я виявив інше на практиці.
Річард Левассер

Ось приклад того, про що згадав Євген Лазін.
землекоп

4
Приховування вашої бази даних за спільною чергою є справді поганим рішенням цього питання, оскільки в цілому SQL і SQLite вже мають вбудовані механізми блокування, які, напевно, набагато вдосконаленіші, ніж будь-що, що ви можете створити спеціально самостійно.
shezi

1
Вам потрібно прочитати питання, на той момент не було вбудованих запірних механізмів. У багатьох сучасних вбудованих базах даних цього механізму не вистачає з міркувань ефективності (наприклад: LevelDB).
Євген Лазін

180

Всупереч поширеній думці, нові версії sqlite3 зробити підтримку доступу з декількох потоків.

Це можна ввімкнути за допомогою додаткового аргументу ключового слова check_same_thread:

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

4
Я стикався з непередбачуваними винятками, і навіть Python аварійно працював із цією опцією (Python 2.7 у Windows 32).
reclosedev

4
Згідно з документами , в багатопотоковому режимі жодне підключення до бази даних не може використовуватися в декількох потоках. Також існує серіалізований режим
Casebash

1
Неважливо, просто знайшов: http://sqlite.org/compile.html#threadsafe
Medeiros

1
@FrEaKmAn, вибачте, це було давно, також ні: memory: database. Після цього я не ділив з'єднання sqlite у декількох потоках.
reclosedev

2
@FrEaKmAn, я зіткнувся з цим, коли процес python здійснював дамп ядра при багатопотоковому доступі. Поведінка була непередбачуваною, і жоден виняток не реєструвався. Якщо я добре пам’ятаю, це стосувалося як читання, так і запису. Це єдине, що я досі бачив, як насправді падає python: D. Я не пробував цього за допомогою sqlite, скомпільованого в режимі безпечного потоку, але на той час я не мав свободи перекомпілювати системний sqlite за замовчуванням. У підсумку я зробив щось подібне до того, що запропонував Ерік, і вимкнув сумісність потоків
детально розказав

17

Наступне знайдено на mail.python.org.pipermail.1239789

Я знайшов рішення. Я не знаю, чому в документації python немає жодного слова про цю опцію. Отже, ми повинні додати новий аргумент ключового слова до функції зв’язку, і ми зможемо створювати з нього курсори в різному потоці. Тому використовуйте:

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

для мене ідеально працює Звичайно, відтепер мені потрібно подбати про безпечний багатопотоковий доступ до бази даних. У будь-якому випадку, все за спроби допомогти.


(Із GIL і справді багатопотоковий доступ до
баз даних і так багато не перешкоджає

ПОПЕРЕДЖЕННЯ : пітон документи були це сказати про check_same_threadопції: «При використанні декількох потоків з тим же з'єднанням операцій записи має бути серіалізовать користувач корупції уникнути даних.» Так що так, ви можете використовувати SQLite з кількома потоками, якщо ваш код гарантує, що лише один потік може записувати в базу даних у будь-який момент часу. Якщо цього не сталося, ви можете пошкодити базу даних.
Ajedi32

14

Перейдіть на багатопроцесорну . Це набагато краще, добре масштабується, може вийти за рамки використання декількох ядер за допомогою декількох процесорів, а інтерфейс такий самий, як використання модуля потокової обробки python.

Або, як запропонував Алі, просто скористайтеся механізмом об'єднання потоків SQLAlchemy . Він обробляє все для вас автоматично і має безліч додаткових функцій, просто щоб процитувати деякі з них:

  1. SQLAlchemy включає діалекти для SQLite, Postgres, MySQL, Oracle, MS-SQL, Firebird, MaxDB, MS Access, Sybase та Informix; IBM також випустила драйвер DB2. Тому вам не доведеться переписувати програму, якщо ви вирішите відійти від SQLite.
  2. Система Unit Of Work, центральна частина реляційного картографування об’єктів (ORM) SQLAlchemy, організовує очікувані операції створення / вставки / оновлення / видалення в черги та змиває їх всім одним пакетом. Для цього він виконує топологічну "сортування залежностей" усіх модифікованих елементів у черзі, щоб дотримуватися обмежень зовнішнього ключа, та групує зайві оператори, де їх іноді можна групувати ще далі. Це забезпечує максимальну ефективність та безпеку транзакцій, а також мінімізує ймовірність тупикових ситуацій.

12

Для цього взагалі не слід використовувати нитки. Це тривіальне завдання для кручених, і воно, мабуть, у будь-якому випадку затягне вас значно далі.

Використовуйте лише один потік, і завершення запиту ініціює подію для запису.

twisted подбає про планування, зворотні дзвінки тощо ... для вас. Він передасть вам весь результат у вигляді рядка, або ви можете запустити його за допомогою потокового процесора (у мене є Twitter-інтерфейс та API Friendfeed, які запускають події для абонентів, оскільки результати все ще завантажуються).

Залежно від того, що ви робите зі своїми даними, ви можете просто звантажити повний результат у sqlite, коли він завершений, приготувати його та звантажити, або приготувати, поки він читається, і скинути його в кінці.

У мене є дуже проста програма, яка робить щось наближене до того, що ви хочете на github. Я називаю це pfetch (паралельне отримання). Він захоплює різні сторінки за розкладом, передає результати у файл і, за бажанням, запускає сценарій після успішного завершення кожної з них. Він також робить деякі вигадливі речі, такі як умовні GET, але все одно може бути хорошою базою для всього, що ви робите.


7

Або якщо ви ліниві, як я, ви можете використовувати SQLAlchemy . Він буде обробляти потоки для вас, ( використовуючи локальний потік та деякий пул з'єднань ), і спосіб його роботи навіть налаштовується .

Для додаткового бонусу, якщо / коли ви зрозумієте / вирішите, що використання Sqlite для будь-якого паралельного додатка стане катастрофою, вам не доведеться міняти свій код, щоб використовувати MySQL, або Postgres, або щось інше. Ви можете просто переключитися.


1
Чому на офіційному веб-сайті ніде не вказана версія Python?
Відображуване ім’я

3

Вам потрібно використовувати session.close()після кожної транзакції до бази даних, щоб використовувати один і той же курсор в одному потоці, не використовуючи той самий курсор у декількох потоках, які спричиняють цю помилку.



0

Мені подобається відповідь Євгена - черги, як правило, найкращий спосіб реалізації міжпотокової комунікації. Для повноти, ось кілька інших варіантів:

  • Закрийте з'єднання DB, коли породжені потоки закінчать його використання. Це виправило б ваші проблеми OperationalError, але відкриття та закриття таких з’єднань, як правило, ні-ні, через накладні витрати.
  • Не використовуйте дочірні нитки. Якщо завдання раз на секунду є досить легким, ви можете піти, виконуючи вибірку та зберігання, а потім спати до потрібного моменту. Це небажано, оскільки операції отримання та зберігання можуть зайняти більше 1 секунди, і ви втратите перевагу мультиплексних ресурсів, що є у вас за допомогою багатопотокового підходу.

0

Вам потрібно розробити паралельність для вашої програми. SQLite має чіткі обмеження, і вам потрібно їх дотримуватися, див. FAQ (також наступне запитання).


0

Скрепінг здається потенційною відповіддю на моє запитання. На його домашній сторінці описано моє точне завдання. (Хоча я ще не впевнений, наскільки стабільний код.)


0

Я б подивився модуль y_serial Python для збереження даних: http://yserial.sourceforge.net

який вирішує проблеми із тупиковими ситуаціями, що оточують єдину базу даних SQLite Якщо попит на паралельність стає великим, можна легко встановити клас Farm з багатьох баз даних, щоб розподілити навантаження протягом стохастичного часу.

Сподіваюся, це допоможе вашому проекту ... він повинен бути досить простим для реалізації за 10 хвилин.


0

Я не міг знайти жодного тесту в жодній із наведених відповідей, тому написав тест, щоб порівняти все.

Я спробував 3 підходи

  1. Читання та запис послідовно з бази даних SQLite
  2. Використання ThreadPoolExecutor для читання / запису
  3. Використання ProcessPoolExecutor для читання / запису

Результати та висновки з еталону такі

  1. Послідовне читання / послідовне записування працюють найкраще
  2. Якщо вам потрібно обробляти паралельно, використовуйте ProcessPoolExecutor для паралельного читання
  3. Не виконуйте жодних записів ні за допомогою ThreadPoolExecutor, ні за допомогою ProcessPoolExecutor, оскільки ви зіткнетеся з помилками, заблокованими в базі даних, і вам доведеться повторити спробу вставити фрагмент ще раз

Ви можете знайти код та повне рішення для тестів у моїй SO відповіді ТУТ Сподіваюся, що це допоможе!


-1

Найімовірніша причина, через яку ви отримуєте помилки із заблокованими базами даних, полягає в тому, що вам потрібно видавати

conn.commit()

після закінчення операції з базою даних. Якщо ви цього не зробите, ваша база даних буде заблокована для запису і буде такою. У інших потоків, які очікують на запис, через деякий час очікується час очікування (за замовчуванням встановлено 5 секунд, див. Http://docs.python.org/2/library/sqlite3.html#sqlite3.connect для отримання детальної інформації про це) .

Прикладом правильної та одночасної вставки може бути такий:

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

Якщо вам подобається SQLite, або у вас є інші інструменти, що працюють з базами даних SQLite, або ви хочете замінити CSV-файли на файли db SQLite, або вам потрібно зробити щось рідкісне, як міжплатформенний IPC, тоді SQLite - чудовий інструмент і дуже підходить для цієї мети. Не дозволяйте натискати на інше рішення, якщо воно здається неправильним!

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.