3.3 KiB
Multithreading Strategies
Coordinate SQLite safely across queues, actors, and Swift concurrency using DataLiteCore.
SQLite remains fundamentally serialized, so deliberate connection ownership and scheduling are essential for correctness and performance. DataLiteCore does not include a built-in connection pool, but its deterministic behavior and configuration options allow you to design synchronization strategies that match your workload.
Core Guidelines
- One connection per queue or actor. Keep each
Connectionconfined to a dedicated serialDispatchQueueor anactorto ensure ordered execution and predictable statement lifecycles. - Do not share statements across threads.
Statementinstances are bound to their parentConnectionand are not thread-safe. - Scale with multiple connections. For concurrent workloads, use a dedicated writer connection alongside a pool of readers so long-running transactions don’t block unrelated operations.
actor Database {
private let connection: Connection
init(path: String) throws {
connection = try Connection(
path: path,
options: [.readwrite, .create, .fullmutex]
)
connection.busyTimeout = 5_000 // wait up to 5 seconds for locks
}
func insertUser(name: String) throws {
let statement = try connection.prepare(
sql: "INSERT INTO users(name) VALUES (?)"
)
try statement.bind(name, at: 1)
try statement.step()
}
}
Encapsulating database work in an actor or serial queue aligns naturally with Swift Concurrency
while maintaining safe access to SQLite’s synchronous API.
Synchronization Options
-
Connection/Options/nomutex— disables SQLite’s internal mutexes (multi-thread mode). Each connection must be accessed by only one thread at a time.let connection = try Connection( location: .file(path: "/path/to/sqlite.db"), options: [.readwrite, .nomutex] ) -
Connection/Options/fullmutex— enables serialized mode with full internal locking. A singleConnectionmay be shared across threads, but global locks reduce throughput.let connection = try Connection( location: .file(path: "/path/to/sqlite.db"), options: [.readwrite, .fullmutex] )
SQLite defaults to serialized mode, but concurrent writers still contend for locks. Plan long
transactions carefully and adjust ConnectionProtocol/busyTimeout to handle SQLITE_BUSY
conditions gracefully.
Delegates and Side Effects
ConnectionDelegate and ConnectionTraceDelegate callbacks execute synchronously on SQLite’s
internal thread. Keep them lightweight and non-blocking. Offload work to another queue when
necessary to prevent deadlocks or extended lock holds.
final class Logger: ConnectionTraceDelegate {
private let queue = DispatchQueue(label: "logging")
func connection(
_ connection: ConnectionProtocol,
trace sql: ConnectionTraceDelegate.Trace
) {
queue.async {
print("SQL:", sql.expandedSQL)
}
}
}
This pattern keeps tracing responsive and prevents SQLite’s internal thread from being blocked by slow I/O or external operations.