Skip to content

Commit 8c10528

Browse files
committed
Add std::future APIs. More unit tests.
1 parent b923765 commit 8c10528

18 files changed

Lines changed: 2876 additions & 473 deletions

CMakeLists.txt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
# Example CMake command line to create project build files:
22
#
3-
# *** Windows ***
4-
# cmake -G "Visual Studio 17 2022" -A Win32 -B Build -S .
5-
#
6-
# *** Linux ***
7-
# cmake -G "Unix Makefiles" -B Build -S .
3+
# cmake -B Build .
84

95
# Specify the minimum CMake version required
106
cmake_minimum_required(VERSION 3.10)
@@ -38,16 +34,19 @@ include_directories(
3834
${DMQ_ROOT_DIR}
3935
SQLite
4036
Port
37+
UnitTest
4138
)
4239

4340
# Add an executable target
4441
add_executable(Async-SQLiteApp ${SOURCES} ${DMQ_LIB_SOURCES})
4542

4643
# Add subdirectories to build
4744
add_subdirectory(SQLite)
45+
add_subdirectory(UnitTest)
4846

4947
target_link_libraries(Async-SQLiteApp PRIVATE
5048
SQLiteLib
49+
UnitTestLib
5150
)
5251

5352

README.md

Lines changed: 95 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ An asynchronous SQLite API wrapper implemented using a C++ delegate libraries. A
1212
- [Asynchronous SQLite API using C++ Delegates](#asynchronous-sqlite-api-using-c-delegates)
1313
- [Table of Contents](#table-of-contents)
1414
- [Overview](#overview)
15-
- [References](#references)
15+
- [References](#references)
1616
- [Getting Started](#getting-started)
1717
- [Delegate Quick Start](#delegate-quick-start)
1818
- [Why Asynchronous SQLite](#why-asynchronous-sqlite)
@@ -23,15 +23,30 @@ An asynchronous SQLite API wrapper implemented using a C++ delegate libraries. A
2323
- [Multithread Example](#multithread-example)
2424
- [Synchronous Blocking Execution](#synchronous-blocking-execution)
2525
- [Asynchronous Non-Blocking Execution](#asynchronous-non-blocking-execution)
26-
- [Test Results](#test-results)
26+
- [Future Example](#future-example)
27+
- [Test Results](#test-results)
2728

2829
# Overview
2930

30-
SQLite is a lightweight, serverless, self-contained SQL database engine commonly used for embedded applications and local data storage. The C++ delegate library is a multi-threaded framework capable of anonymously targeting any callable function, either synchronously or asynchronously. This delegate library is used to create an asynchronous API for SQLite. SQLite operates in a private thread of control, with all client API calls being invoked asynchronously on this private thread. The SQLite asynchronous wrapper makes no changes to the SQLite API, except for the addition of a trailing timeout argument for wait duration.
31+
SQLite is a lightweight, serverless, self-contained SQL database engine commonly used for embedded applications and local data storage. The C++ DelegateMQ library is a multi-threaded framework capable of anonymously targeting any callable function, either synchronously or asynchronously.
3132

32-
The purpose of the wrapper is twofold: First, to provide a simple asynchronous layer over SQLite, and second, to serve as a working example of an asynchronous delegate library or subsystem interface.
33+
This project leverages the delegate library to create a robust, thread-safe asynchronous wrapper for SQLite. All database operations are marshaled to a dedicated private worker thread, ensuring strict serialization of database access while freeing the client thread from blocking operations.
3334

34-
# References
35+
**Key Features**
36+
37+
1. **Thread Safety:** A single background thread manages all SQLite interactions, preventing race conditions without complex client-side locking.
38+
39+
2. **Synchronous API (Blocking):** Wrappers that mimic the standard SQLite API but execute on the worker thread. The calling thread blocks until completion or a specified timeout occurs.
40+
41+
* *Usage:* Ideal for linear logic where the result is needed immediately.
42+
* *Signature:* `sqlite3_exec(..., dmq::Duration timeout)`
43+
44+
3. **Future API (Non-Blocking):** "Fire-and-Forget" functions that return a `std::future<int>` immediately. The main thread can continue processing UI updates or other tasks while the heavy SQL operation runs in the background.
45+
46+
* *Usage:* Ideal for high-performance applications, UI responsiveness, and concurrent task processing.
47+
* *Signature:* `std::future<int> sqlite3_exec_future(...)`
48+
49+
## References
3550

3651
* <a href="https://github.com/endurodave/DelegateMQ">DelegatesMQ</a> - Invoke any C++ callable function synchronously, asynchronously, or on a remote endpoint.
3752
* <a href="https://www.sqlite.org/">SQLite</a> - SQLite is a C-language library that implements a small, fast, self-contained, high-reliability, full-featured, SQL database engine.
@@ -82,16 +97,21 @@ int main(void)
8297
8398
# Why Asynchronous SQLite
8499
85-
* **Improved Performance:** Offloading SQLite operations to a separate thread allows the main thread to remain responsive, enhancing overall application performance.
86-
* **Non-blocking Operations:** By executing database queries in a separate thread, the main application thread can continue processing other tasks without waiting for SQLite operations to complete. A callback can be used to signal completion.
87-
* **Atomic Operations:** Execute a series of database operations that cannot be interrupted without locks.
88-
* **Isolation:** Running SQLite on a private thread ensures that database-related tasks are isolated from the main application logic, reducing the risk of thread contention or deadlock in the main application.
100+
* **Thread Safety by Design:** SQLite handles are accessed exclusively by a single private worker thread. This serialization eliminates race conditions and removes the need for complex mutex locking code in your main application.
101+
102+
* **Responsive UI (Non-Blocking):** Using the Future API, expensive database operations (like bulk inserts or complex joins) are offloaded to the background. The main thread receives a `std::future` immediately, allowing it to keep the UI smooth and responsive while waiting for the result.
103+
104+
* **Flexible Control Flow:** The wrapper provides two distinct ways to interact with the database:
89105
90-
A side benefit is a real-world example of a multi-threaded application using the delegate library.
106+
* **Synchronous:** Block and wait for a result (simple, linear logic for standard tasks).
107+
108+
* **Asynchronous:** "Fire-and-forget" using futures (high-concurrency for heavy tasks).
109+
110+
* **Sequential Atomicity:** Requests sent to the worker thread are executed in the exact order they are received. This allows you to safely dispatch a chain of dependent commands (e.g., `BEGIN`, multiple `INSERT`s, `COMMIT`) knowing they will run uninterrupted.
91111
92112
# Asynchronous SQLite
93113
94-
The file `async_sqlite3.h` implement the asynchronous interface. Each async function matches the SQLite library except the addition of a `timeout` argument.
114+
The file `async_sqlite3.h` defines the asynchronous interface. Each async function matches the SQLite library except the addition of a `timeout` argument.
95115
96116
```cpp
97117
namespace async
@@ -108,7 +128,7 @@ namespace async
108128
SQLITE_API int sqlite3_open(
109129
const char* filename, /* Database filename (UTF-8) */
110130
sqlite3** ppDb, /* OUT: SQLite db handle */
111-
std::chrono::milliseconds timeout = MAX_WAIT
131+
dmq::Duration timeout = MAX_WAIT
112132
);
113133
114134
SQLITE_API int sqlite3_exec(
@@ -117,12 +137,12 @@ namespace async
117137
sqlite3_callback xCallback, /* Invoke this callback routine */
118138
void* pArg, /* First argument to xCallback() */
119139
char** pzErrMsg, /* Write error messages here */
120-
std::chrono::milliseconds timeout = MAX_WAIT
140+
dmq::Duration timeout = MAX_WAIT
121141
);
122142
123143
SQLITE_API int sqlite3_close(
124144
sqlite3* db,
125-
std::chrono::milliseconds timeout = MAX_WAIT
145+
dmq::Duration timeout = MAX_WAIT
126146
);
127147
128148
// etc...
@@ -146,12 +166,11 @@ Thread* async::sqlite3_get_thread(void)
146166
SQLITE_API int async::sqlite3_open(
147167
const char* filename, /* Database filename (UTF-8) */
148168
sqlite3** ppDb, /* OUT: SQLite db handle */
149-
std::chrono::milliseconds timeout
169+
dmq::Duration timeout
150170
)
151171
{
152172
// Asynchronously invoke ::sqlite3_open on the SQLiteThread thread
153-
auto retVal = AsyncInvoke(::sqlite3_open, timeout, filename, ppDb);
154-
return retVal;
173+
return AsyncInvoke(::sqlite3_open, timeout, filename, ppDb);
155174
}
156175

157176
SQLITE_API int async::sqlite3_exec(
@@ -160,84 +179,23 @@ SQLITE_API int async::sqlite3_exec(
160179
sqlite3_callback xCallback, /* Invoke this callback routine */
161180
void* pArg, /* First argument to xCallback() */
162181
char** pzErrMsg, /* Write error messages here */
163-
std::chrono::milliseconds timeout
182+
dmq::Duration timeout
164183
)
165184
{
166-
auto retVal = AsyncInvoke(::sqlite3_exec, timeout, db, zSql, xCallback, pArg, pzErrMsg);
167-
return retVal;
185+
return AsyncInvoke(::sqlite3_exec, timeout, db, zSql, xCallback, pArg, pzErrMsg);
168186
}
169187

170188
SQLITE_API int async::sqlite3_close(
171189
sqlite3* db,
172-
std::chrono::milliseconds timeout
190+
dmq::Duration timeout
173191
)
174192
{
175-
auto retVal = AsyncInvoke(::sqlite3_close, timeout, db);
176-
return retVal;
193+
return AsyncInvoke(::sqlite3_close, timeout, db);
177194
}
178195

179196
// etc...
180197
```
181198

182-
The `AsyncInvoke()` helper function invokes the function asynchronously if the caller is not executing on the `SQLiteThread` thread. Otherwise, if the caller is already on the internal thread the target function is called synchronously.
183-
184-
```cpp
185-
// A private worker thread instance to execute all SQLite API functions
186-
static Thread SQLiteThread("SQLite Thread");
187-
188-
/// Helper function to simplify asynchronous function calling on SQLiteThread
189-
/// @param[in] func - a function to invoke
190-
/// @param[in] timeout - the time to wait for invoke to complete
191-
/// @param[in] args - the function argument(s) passed to func
192-
template <typename Func, typename Timeout, typename... Args>
193-
auto AsyncInvoke(Func func, Timeout timeout, Args&&... args)
194-
{
195-
// Deduce return type of func
196-
using RetType = decltype(func(std::forward<Args>(args)...));
197-
198-
// Is the calling function executing on the SQLiteThread thread?
199-
if (SQLiteThread.GetThreadId() != Thread::GetCurrentThreadId())
200-
{
201-
// Create a delegate that points to func and is invoked on SQLiteThread
202-
auto delegate = MakeDelegate(func, SQLiteThread, timeout);
203-
204-
// Invoke the delegate target function asynchronously and wait for function call to complete
205-
auto retVal = delegate.AsyncInvoke(std::forward<Args>(args)...);
206-
207-
if constexpr (std::is_void<RetType>::value == false)
208-
{
209-
// Did async function call succeed?
210-
if (retVal.has_value())
211-
{
212-
// Return the return value to caller
213-
return std::any_cast<RetType>(retVal.value());
214-
}
215-
else
216-
{
217-
if constexpr (std::is_same_v<RetType, int>)
218-
return SQLITE_ERROR; // Special case for int
219-
else
220-
return RetType();
221-
}
222-
}
223-
}
224-
else
225-
{
226-
// Invoke target function synchronously since we're already executing on SQLiteThread
227-
if constexpr (std::is_void_v<RetType>)
228-
{
229-
func(std::forward<Args>(args)...); // Synchronous call
230-
return RetType(); // No return value for void
231-
}
232-
else
233-
{
234-
auto retVal = func(std::forward<Args>(args)...); // Return the result for non-void types
235-
return std::any_cast<RetType>(retVal);
236-
}
237-
}
238-
}
239-
```
240-
241199
# Examples
242200

243201
The `main()` function executes example code.
@@ -264,6 +222,7 @@ int main(void)
264222
example2();
265223
auto blockingDuration = example3();
266224
auto nonBlockingDuration = example4();
225+
example_future();
267226

268227
// Wait for example4() to complete on nonBlockingAsyncThread
269228
while (!completeFlag)
@@ -567,7 +526,60 @@ std::chrono::microseconds example4()
567526
}
568527
```
569528

570-
### Test Results
529+
## Future Example
530+
531+
This example illustrates how to utilize `std::future` for concurrent database operations. By leveraging the asynchronous API, you can implement a "Fire-and-Forget" pattern where the main thread remains fully responsive—performing UI updates or other processing—while heavy SQL operations execute in the background.
532+
533+
```cpp
534+
void example_future()
535+
{
536+
printf_safe("\n--- Starting Example 5 (Future/Async) ---\n");
537+
538+
sqlite3* db = nullptr;
539+
// Open DB synchronously to ensure valid handle
540+
async::sqlite3_open("async_future_example.db", &db);
541+
542+
// Create a table synchronously (we need it before inserting)
543+
async::sqlite3_exec(db, "CREATE TABLE IF NOT EXISTS heavy_data (id INT, val TEXT);", nullptr, nullptr, nullptr);
544+
545+
// 1. Launch a heavy insert operation asynchronously
546+
// This returns immediately, giving us a std::future
547+
printf_safe("[Main] Launching heavy async insert...\n");
548+
549+
// Note: The SQL string must remain valid until the future completes.
550+
// Ideally use string literals or manage lifetime carefully.
551+
std::string sql = "INSERT INTO heavy_data VALUES (123, 'Concurrent Data');";
552+
553+
// Pass 5 arguments matching the raw API signature
554+
auto future = async::sqlite3_exec_future(db, sql.c_str(), nullptr, nullptr, nullptr);
555+
556+
// 2. Perform other work on the main thread while DB is busy
557+
// In a real app, this would be UI updates or input processing.
558+
printf_safe("[Main] DB is busy. Performing other tasks on main thread...\n");
559+
for (int i = 0; i < 3; ++i) {
560+
std::this_thread::sleep_for(std::chrono::milliseconds(10));
561+
printf_safe("[Main] Working... %d%%\n", (i + 1) * 33);
562+
}
563+
564+
// 3. Wait for the database operation to complete and check the result
565+
printf_safe("[Main] Waiting for DB to finish...\n");
566+
567+
// .get() blocks here ONLY if the DB task is still running.
568+
int rc = future.get();
569+
570+
if (rc == SQLITE_OK) {
571+
printf_safe("[Main] Async insert completed successfully!\n");
572+
}
573+
else {
574+
printf_safe("[Main] Async insert failed with code: %d\n", rc);
575+
}
576+
577+
async::sqlite3_close(db);
578+
std::remove("async_future_example.db");
579+
}
580+
```
581+
582+
## Test Results
571583

572584
Use *DB Browser for SQLite* tool to examine the results. Notice the interleaving of data by both worker threads.
573585

0 commit comments

Comments
 (0)