两个不同线程中两个 SQLite 连接的并发问题

Concurrency issue with two SQLite connections in two different Threads

我正在使用 System.Data.SQLite 和 C#

我有

线程 1 (UI) - 写入 table1 线程 2(worker)写入 table1

所以我有两个线程同时写入同一个数据库。我按批次写入数据,每个批次都有一个事务。需要批处理以避免锁定数据库的时间过长,以便其他线程可以对数据库进行写访问。

但这不起作用。我希望 thread1 能够在 thread2 的事务批次之间写入数据库,但这不会发生,除非我在批次之间有 Thread.Sleep(100)。请注意,Thread.Sleep(10) 的值较小也不起作用。我知道这与线程上下文切换有关,但我不明白为什么少量 Thread.Sleep 不能完成这项工作。

有没有办法控制谁获得数据库锁的优先级,因为使用Thread.Sleep不好?

P.S。即使没有交易,这似乎也是一个问题。如果我有一个包含许多插入语句的循环,如果没有 Thread.Sleep

,另一个线程将无法在插入语句之间执行任何操作

看看busy_timeout。在理想的世界中,您的两个线程(假设它们不 而不是 共享连接)都应该被允许在它们方便的时候读写。如果可以避免,为什么还要用 Sleep 计时?

接下来,您正在正确使用交易。您是否研究过交易的三种不同行为? https://sqlite.org/lang_transaction.html

然而,这并没有解决线程 1 可能在线程 2 正在尝试获取锁的问题。为此,请参阅 PRAGMA busy_timeout。只需将每个连接上的 pragma 设置为 1000 (ms)。如果线程 2 已锁定数据库并且线程 1 尝试获取锁,它将简单地等待 1000 毫秒,直到因超时错误而失败。 (https://sqlite.org/pragma.html#pragma_busy_timeout)

我认为 SQlite 不支持真正的并发写入事务...我在 Android 上使用了 "non-exclusive" 事务并且它们被记录在案(我是用我自己的话写的内存)允许并发读取,而可能正在进行写入。

现在更重要了...查看 SQLite 事务文档:

https://sqlite.org/lang_transaction.html

Thus with a deferred transaction, the BEGIN statement itself does nothing to the filesystem. Locks are not acquired until the first read or write operation. The first read operation against a database creates a SHARED lock and the first write operation creates a RESERVED lock. Because the acquisition of locks is deferred until they are needed, it is possible that another thread or process could create a separate transaction and write to the database after the BEGIN on the current thread has executed. If the transaction is immediate, then RESERVED locks are acquired on all databases as soon as the BEGIN command is executed, without waiting for the database to be used.

(强调 - 我的)

好的,现在我们已经知道写入数据库需要保留锁。

让我们看看这里是什么:

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

A RESERVED lock means that the process is planning on writing to the database file at some point in the future but that it is currently just reading from the file. Only a single RESERVED lock may be active at one time, though multiple SHARED locks can coexist with a single RESERVED lock. RESERVED differs from PENDING in that new SHARED locks can be acquired while there is a RESERVED lock.

好的,所以这证实了 SQLite 需要一个保留锁来写入数据库,并且还告诉我们一次只能存在一个保留锁 -> 只允许一个事务具有写访问权限,其他事务会等。

现在,如果您尝试从两个线程交错写入事务,每个线程执行多个(粒度)事务 - 那么这是一个想法:

  • 将 Thread.Sleep 替换为 Thread.Yield

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

这可能有助于解决 "the current writing thread's Sleep didn't cause a context switch to the thread we want" 的问题。

即使使用 Yield 仍然无法保证 OS / 运行 时间会切换到您想要的线程,但是...也许值得一试,至少您不会'不要人为地使您的代码 运行 变慢。

  • 鉴于我们对 SQLite 的了解 "only one transaction is allowed to write",我会考虑以下模式:

1 - 创建一个新线程,其工作是处理数据库写入

2 - 从您当前的两个线程排队到该线程的写操作

3 - 让 "operations" 成为自包含/足够的对象,包含他们打算写入的所有数据

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 基础 class 有一个 CountDownEvent,它 1) 由 awaitCompletion 等待,2) 由 signalCompletion 发出信号。

我确定有一种方法可以将它包装成更优雅的助手 classes 并且可能使用 async / await。