19、多线程与数据库:SQLite线程模式、连接池设计、线程安全操作、互斥锁使用

多线程环境下操作数据库,是很多C程序员头疼的事。说实话,我早年做嵌入式项目时,就因为在多线程里直接共享同一个SQLite连接,导致数据错乱,排查了整整两天。从那以后,我对线程安全这块就格外上心。

这一章,咱们就聊聊SQLite在多线程下的那些事。包括它的线程模式、怎么设计一个轻量级的连接池、以及互斥锁的正确用法。

19.1 SQLite的三种线程模式

SQLite本身支持三种线程模式,你可以在编译时或运行时选择。我个人习惯在编译时直接指定,省得运行时还要操心。

模式 宏定义 说明
单线程模式 SQLITE_THREADSAFE=0 所有互斥锁被禁用,性能最高,但只能在单线程中使用
多线程模式 SQLITE_THREADSAFE=1 单个连接不能被多线程共享,但不同线程可用不同连接
序列化模式 SQLITE_THREADSAFE=2 完全线程安全,一个连接可以被多个线程安全使用

重点:序列化模式虽然方便,但内部加锁开销大。我在项目中更常用多线程模式 + 连接池,性能更好,控制也更灵活。

19.2 连接池设计思路

你想想看,如果每个线程都自己打开一个数据库连接,那连接数会随着线程数暴涨。嵌入式设备资源有限,扛不住的。

连接池的核心思想很简单:预先创建一批连接,线程需要时借,用完还回去。这样既控制了连接总数,又避免了频繁打开关闭的开销。

我设计过一个轻量级连接池,结构大概是这样:

typedef struct {
    sqlite3 **conns;      // 连接数组
    int *available;       // 可用标记,1表示空闲
    int total;            // 总连接数
    pthread_mutex_t mutex; // 互斥锁
    pthread_cond_t cond;   // 条件变量
} ConnectionPool;

初始化时,我一次性创建 total 个连接。每个线程调用 pool_get() 时,从池里找一个空闲连接;用完调用 pool_put() 归还。

小技巧:连接数一般设为 CPU 核心数 + 1 或 2。我在四核的ARM板子上试过,5个连接就够用了。太多反而因为锁竞争导致性能下降。

19.3 线程安全操作与互斥锁

连接池本身需要互斥锁保护,不然多个线程同时取连接,数据就乱了。我习惯用 pthread_mutex_t,简单可靠。

下面是一个取连接的示例:

sqlite3* pool_get(ConnectionPool *pool) {
    pthread_mutex_lock(&pool->mutex);
    for (int i = 0; i < pool->total; i++) {
        if (pool->available[i]) {
            pool->available[i] = 0;
            pthread_mutex_unlock(&pool->mutex);
            return pool->conns[i];
        }
    }
    // 没有空闲连接,等待
    pthread_cond_wait(&pool->cond, &pool->mutex);
    // 唤醒后递归获取
    pthread_mutex_unlock(&pool->mutex);
    return pool_get(pool);
}

嗯,这里要注意:pthread_cond_wait 会释放锁,被唤醒后重新获取。这个机制能避免忙等待,节省CPU。

我曾经踩过的坑:在归还连接时忘记解锁,导致其他线程永远拿不到连接。后来我养成了一个习惯——每次加锁后,都在心里默念一遍「记得解锁」。你也可以用 RAII 风格封装一下,C语言里用 goto 做统一出口也行。

19.4 核心知识体系

下面这张图,是我梳理的多线程与SQLite的核心逻辑。你看一遍,基本就心里有数了。

多线程与SQLite核心知识体系 线程模式 • 单线程模式 • 多线程模式 • 序列化模式 • 编译时/运行时选择 连接池设计 • 预创建连接 • 借/还机制 • 连接数控制 • 条件变量等待 线程安全操作 • 互斥锁保护 • 原子操作 • 事务隔离 • 错误处理 实践要点 1. 根据场景选择线程模式,嵌入式推荐多线程模式 2. 连接池大小 = CPU核心数 + 1 或 2 3. 加锁后务必解锁,推荐统一出口模式 4. 条件变量避免忙等待,提高CPU利用率

19.5 完整示例:多线程写入

最后,我贴一个实际用过的多线程写入示例。两个线程同时往数据库里写数据,通过连接池保证安全。

#include <stdio.h>
#include <pthread.h>
#include <sqlite3.h>

#define NUM_THREADS 2
#define POOL_SIZE 3

typedef struct {
    sqlite3 *conns[POOL_SIZE];
    int available[POOL_SIZE];
    pthread_mutex_t mutex;
    pthread_cond_t cond;
} ConnectionPool;

ConnectionPool pool;

void* worker(void *arg) {
    int id = *(int*)arg;
    sqlite3 *db = pool_get(&pool);
    
    char sql[128];
    snprintf(sql, sizeof(sql), 
             "INSERT INTO logs VALUES (%d, 'thread %d');", id, id);
    
    char *err = NULL;
    sqlite3_exec(db, sql, NULL, NULL, &err);
    if (err) {
        fprintf(stderr, "SQL error: %s\n", err);
        sqlite3_free(err);
    }
    
    pool_put(&pool, db);
    return NULL;
}

int main() {
    // 初始化连接池
    pthread_mutex_init(&pool.mutex, NULL);
    pthread_cond_init(&pool.cond, NULL);
    for (int i = 0; i < POOL_SIZE; i++) {
        sqlite3_open("test.db", &pool.conns[i]);
        pool.available[i] = 1;
    }
    
    pthread_t threads[NUM_THREADS];
    int ids[NUM_THREADS];
    for (int i = 0; i < NUM_THREADS; i++) {
        ids[i] = i;
        pthread_create(&threads[i], NULL, worker, &ids[i]);
    }
    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
    }
    
    // 清理
    for (int i = 0; i < POOL_SIZE; i++) {
        sqlite3_close(pool.conns[i]);
    }
    pthread_mutex_destroy(&pool.mutex);
    pthread_cond_destroy(&pool.cond);
    return 0;
}

核心总结:多线程操作SQLite,说白了就是三件事——选对线程模式、设计好连接池、锁要加对。我在项目中一直用这个套路,从没出过问题。你试试看,有问题随时调。

公众号:蓝海资料掘金营,微信 deep3321