|
| 1 | +# Thread safety and concurrent database access |
| 2 | + |
| 3 | +There are two layers to consider regarding concurrent database access and thread safety: the SQLite layer and the `sqlite_orm` layer. |
| 4 | + |
| 5 | +## 1. The SQLite layer |
| 6 | + |
| 7 | +Everything depends on how SQLite was compiled. |
| 8 | + |
| 9 | +`sqlite_orm` does not add any layer to SQLite's multi-threading policy or capability, nor does it impose additional restrictions. |
| 10 | + |
| 11 | +At the time of writing, there are no runtime database connection options in `sqlite_orm` that change SQLite's concurrent access modes. |
| 12 | + |
| 13 | +Other SQLite features affect concurrent database access, data visibility, and on-disk integrity; for example: journal mode (WAL), handling the `busy` state, transactions, and syncing to disk. |
| 14 | + |
| 15 | +Schema synchronization or data operations that require coherent, atomic behavior must be performed inside a transaction and often require a busy handler. This is especially important when accessing a database from multiple processes. |
| 16 | + |
| 17 | +Familiarize yourself with SQLite's threading and concurrency documentation for full details: |
| 18 | +- [Can multiple applications or multiple instances of the same application access a single database file at the same time?](https://www.sqlite.org/faq.html#q5) — yes; SQLite uses file locks to coordinate concurrent access to the database file itself. Restrictions apply. |
| 19 | +- [Is SQLite threadsafe?](https://www.sqlite.org/faq.html#q6) — yes; SQLite uses locks for a database connection. |
| 20 | +- [Using SQLite In Multi-Threaded Applications](https://www.sqlite.org/threadsafe.html) |
| 21 | + |
| 22 | +## 2. The `sqlite_orm` layer |
| 23 | + |
| 24 | +A program commonly uses a single `storage` instance created by `sqlite_orm::make_storage()` to represent the database. As of `sqlite_orm` v1.10, a `storage` instance can be safely shared; see the considerations below. |
| 25 | + |
| 26 | +Unless you are using an in-memory database, no connection is established by default when you define the `storage` object. On each database access, a connection is opened and closed automatically. It is, of course, possible to open the database permanently, as described in the "A word about performance" section below. |
| 27 | + |
| 28 | +Background: each `storage` instance maintains an atomic connection counter and a handle to the database (pointer). When the counter increases from 0 to 1 the database is opened; when it drops from 1 to 0 the database is closed. As of `sqlite_orm` v1.10, this process is always synchronized (i.e., atomic). For the slow path that involves opening the database, the process of establishing a connection is performed under a mutually exclusive lock. When a connection already exists, the fast path can be taken, which only involves atomic reference counting. |
| 29 | + |
| 30 | +### Special considerations |
| 31 | + |
| 32 | +As mentioned above, a `storage` instance may be shared across threads to concurrently perform queries or CRUD operations. |
| 33 | + |
| 34 | +However, there are things you should only do from a single-threaded context, preferably at the beginning of your program: |
| 35 | + |
| 36 | +1. Synchronization of the database schema. |
| 37 | +2. Actions for which `sqlite_orm` must keep state independent of whether a database connection exists: |
| 38 | + a. Creation or deletion of application-defined scalar, aggregate, and collating functions with `storage.create_scalar_function()`, `storage.delete_scalar_function()`, `storage.create_aggregate_function()`, `storage.delete_aggregate_function()`, `storage.create_quoted_scalar_function()`, `storage.delete_quoted_scalar_function()`, `storage.create_collate_function()`, and `storage.delete_collate_function()`. |
| 39 | + b. Configuration of database limits with `storage.limit.set()`. |
| 40 | + c. Installation of the busy handler with `storage.busy_handler()`. |
| 41 | + d. Installation of an 'on open' handler with `storage.on_open`. |
| 42 | + e. Opening a permanent connection to a database on disk by calling `storage.open_forever()`. |
| 43 | + |
| 44 | +## A word about performance |
| 45 | + |
| 46 | +Opening and setting up a database connection has nontrivial overhead. |
| 47 | + |
| 48 | +If your program accesses the database frequently, especially from multiple threads, open the database "forever" — preferably by passing connection control options when creating the `storage` instance: |
| 49 | +``` |
| 50 | +auto storage = make_storage("", connection_control{true}); |
| 51 | +``` |
| 52 | +... or call: |
| 53 | +``` |
| 54 | +storage.open_forever(); |
| 55 | +``` |
| 56 | + |
| 57 | +Advantages: |
| 58 | +1. No locking is required in the fast path, which helps the CPU branch predictor. |
| 59 | +2. Connection setup (for example, configuring limits or registering application-defined functions) happens only once. |
0 commit comments