Проблема параллелизма с двумя соединениями SQLite в двух разных потоках - PullRequest
0 голосов
/ 28 ноября 2018

Я использую System.Data.SQLite с C #

У меня есть

Поток 1 (пользовательский интерфейс) - запись в таблицу1 Поток 2 (рабочий) пишет в таблицу1

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

Но это не работает.Я надеялся, что thread1 сможет записывать в базу данных между пакетами транзакций thread2, но этого не произойдет, если у меня между потоками не будет Thread.Sleep (100).Обратите внимание, что небольшое значение для Thread.Sleep (10) также не работает.Я понимаю, что это связано с переключением контекста потока, но я не понимаю, почему небольшое количество Thread.Sleep не выполняет эту работу.

Есть ли способ контролировать приоритет того, кто получает блокировку базы данных,потому что использовать Thread.Sleep плохо?

PS Кажется, это проблема даже без транзакций.Если у меня есть цикл со многими операторами вставки, другой поток не может выполнить что-либо между инструкциями вставки, если нет Thread.Sleep

Ответы [ 2 ]

0 голосов
/ 30 ноября 2018

Я не думаю, что SQlite поддерживает истинные параллельные транзакции записи ... Я использовал «неисключительные» транзакции на Android, и они документированы (я пишу своими словами из памяти) как разрешающие параллельные чтенияв то время как может происходить запись.

Теперь ближе к делу ... Рассмотрим документы транзакций SQLite:

https://sqlite.org/lang_transaction.html

Таким образом, с помощьюотложенная транзакция, сама инструкция BEGIN ничего не делает с файловой системой.Блокировки не получаются до первой операции чтения или записи.Первая операция чтения для базы данных создает блокировку SHARED, а первая операция записи создает блокировку RESERVED .Поскольку получение блокировок откладывается до тех пор, пока они не потребуются, возможно, что другой поток или процесс может создать отдельную транзакцию и выполнить запись в базу данных после выполнения BEGIN в текущем потоке.Если транзакция немедленная, то RESERVED блокировки получаются во всех базах данных , как только выполняется команда BEGIN, без ожидания использования базы данных.

(выделение -мой)

ОК, теперь мы узнали, что для записи в базу данных требуется зарезервированная блокировка.

Давайте посмотрим, что это здесь:

https://sqlite.org/lockingv3.html#reserved_lock

Блокировка RESERVED означает, что процесс планирует запись в файл базы данных в какой-то момент в будущем, но в данный момент он просто читает из файла. Только одна зарезервированная блокировка может быть активной одновременно, хотя несколько блокировок SHARED могут сосуществовать с одной зарезервированной блокировкой.RESERVED отличается от PENDING тем, что новые блокировки SHARED могут быть получены при наличии блокировки RESERVED.

OK, поэтому это подтверждает, что SQLite требует блокировки RESERVED для записи в базу данных, а также сообщает нам, чтоодновременно может существовать только одна зарезервированная блокировка -> только одной транзакции разрешено иметь доступ на запись, другие будут ждать.

Теперь, если вы пытаетесь чередовать записи транзакций из двух потоков, каждый поток выполняетсянесколько (гранулярных) транзакций - тогда вот идея:

  • Замена Thread.Sleep с Thread.Yield

https://docs.microsoft.com/en-us/dotnet/api/system.threading.thread.yield

Это может помочьс вопросом «Сон текущего записывающего потока не вызвал переключение контекста на нужный нам поток».

Даже с выходом все еще нет никаких гарантий, что ОС / среда выполнения переключится на нужный вам поток, но ... возможно, стоит попробовать, и, по крайней мере, вы не будете искусственно замедлять работу вашего кода.

  • Учитывая то, что мы знаем оSQLite «разрешена запись только одной транзакции», я хотел бы рассмотреть следующий шаблон:

1 - создать новый поток, задачей которого является обработка записей в базу данных

2 - очередьоперации записи в этот поток из ваших текущих двух потоков

3 - «Операции» должны быть автономными / достаточными объектами, содержащими все данные, которые они намереваются записать

4 - Наконец,используйте обратный вызов с защелкой (в C # это CountDownEvent, я считаю), чтобы узнать, когда операция завершена, поэтому ваши текущие потоки могут ожидать завершения

Тогда у вас будет только один поток записи (поскольку SQliteи все еще имеют параллелизм между вашими текущими потоками.

Псевдокод:

// Write thread

while (item = blockingQueue.getNextItemToWrite()) {
 item.executeWrite(database)
 item.signalCompletion()
}

// Thread 1

item = new WriteItem(some data that needs to be written)
WriteThread.enqeue(item)
item.awaitCompletion()

// Thread 2 - same as Thread 1

Где базовый класс WriteItem имеет значение CountDownEvent, равное 1), ожидаемое awaitCompletion, и 2) сигнализируемоеsignalCompletion.

Я уверен, что есть способ обернуть это в более элегантные вспомогательные классы и, возможно, использовать async / await.

0 голосов
/ 28 ноября 2018

Посмотрите на busy_timeout.В идеальном мире обе ваши темы (при условии, что они не , а не разделяют соединение) должны иметь возможность читать и писать по своему усмотрению.Зачем беспокоиться о времени сна, если вы можете избежать этого?

Далее, вы правильно используете транзакции.Вы рассматривали три различных поведения для транзакций?https://sqlite.org/lang_transaction.html

Это, однако, не решает проблему, что поток 1 может попытаться получить блокировку, в то время как поток 2 это делает.Для этого см. PRAGMA busy_timeout.Просто поместите прагму в каждое соединение, скажем, в 1000 (мс).Если поток 2 заблокировал базу данных, а поток 1 пытается получить блокировку, он просто будет ждать 1000 мс, пока не завершится с ошибкой тайм-аута.(https://sqlite.org/pragma.html#pragma_busy_timeout)

...