【C++】手写 MySQL 连接池(三):异步任务队列与工作线程

同步查询会阻塞当前线程,只有 SQL 执行完成并返回结果后,当前线程才能继续向下运行。

在一些服务器程序中,业务线程可能只需要提交数据库操作,并不希望一直等待。例如:

复制代码
接收客户端请求
        ↓
提交数据库更新任务
        ↓
继续处理其他客户端请求
        ↓
后台线程执行SQL

这种情况下,可以把 SQL 封装成任务,放入线程安全队列,再由数据库工作线程从队列中取出并执行。

本文重点介绍异步连接池中的三个核心部分:

复制代码
SQLOperation:统一描述不同类型的SQL任务
ProducerConsumerQueue:保存等待执行的任务
DatabaseWorker:后台获取并执行SQL任务

一、异步数据库连接池的运行方式

同步查询的基本流程是:

复制代码
业务线程
   ↓
获取同步连接
   ↓
执行SQL
   ↓
等待MySQL返回结果
   ↓
继续执行后续代码

异步查询则将任务的提交与执行分开:

复制代码
业务线程
   ↓
创建SQL任务
   ↓
将任务放入队列
   ↓
继续处理其他业务

后台工作线程
   ↓
从队列中取出任务
   ↓
绑定数据库连接
   ↓
执行SQL
   ↓
保存查询结果

异步连接池中的多个连接会共享同一个任务队列:

复制代码
                         SQL任务队列
                     task1 → task2 → task3
                         ↑
              业务线程不断提交任务
                         ↓
        ┌────────────────┼────────────────┐
        ↓                ↓                ↓
  DatabaseWorker1  DatabaseWorker2  DatabaseWorker3
        ↓                ↓                ↓
  MySQL连接1        MySQL连接2        MySQL连接3

每一个异步 MySQL 连接都绑定一个独立的 DatabaseWorker

复制代码
MySQLConnection::MySQLConnection(
    ProducerConsumerQueue<SQLOperation*>* queue,
    MySQLConnectionInfo& connInfo)
    : m_queue(queue),
      m_Mysql(nullptr),
      m_connectionInfo(connInfo),
      m_connectionFlags(CONNECTION_ASYNC)
{
    /*
     * 当前异步连接创建一个专属工作线程。
     *
     * 工作线程固定使用this所表示的MySQL连接,
     * 但所有异步连接共享同一个SQL任务队列。
     */
    m_worker = std::make_unique<DatabaseWorker>(
        m_queue,
        this
    );
}

假设创建了 8 个异步连接,就会同时创建 8 个后台工作线程。

这些工作线程共同竞争队列中的任务,但每个工作线程只使用自己绑定的数据库连接:

复制代码
工作线程1固定使用连接1
工作线程2固定使用连接2
工作线程3固定使用连接3
......

因此,异步连接不需要像同步连接一样,反复调用 GetFreeConnection() 查找空闲连接。

任务由哪个工作线程取走,就由该线程绑定的 MySQL 连接执行。


二、SQLOperation 如何统一表示 SQL 任务

异步任务可能有多种类型,例如:

复制代码
普通字符串SQL
预处理语句
异步查询
数据库事务
批量查询
连接保活任务

如果工作线程分别判断每一种任务类型,代码会非常复杂。

因此,可以定义一个统一的任务基类:

复制代码
class SQLOperation
{
public:
    SQLOperation()
        : m_conn(nullptr)
    {
    }

    virtual ~SQLOperation() = default;

    /*
     * 工作线程统一调用call()执行任务。
     *
     * call内部再调用子类重写的Execute()。
     */
    virtual int call()
    {
        Execute();
        return 0;
    }

    /*
     * 不同SQL任务需要实现自己的Execute()。
     */
    virtual bool Execute() = 0;

    /*
     * 在任务执行前,为任务绑定MySQL连接。
     */
    virtual void SetConnection(MySQLConnection* connection)
    {
        m_conn = connection;
    }

protected:
    // 当前任务最终使用的数据库连接
    MySQLConnection* m_conn;

private:
    // SQL任务不能被复制
    SQLOperation(const SQLOperation&) = delete;
    SQLOperation& operator=(const SQLOperation&) = delete;
};

SQLOperation 是一个抽象基类,因为它包含纯虚函数:

复制代码
virtual bool Execute() = 0;

工作线程不需要知道任务具体是什么类型,只需要统一执行:

复制代码
operation->SetConnection(_connection);
operation->call();

其中:

复制代码
operation->SetConnection(_connection);

负责给任务绑定当前工作线程所管理的数据库连接。

下面的代码:

复制代码
operation->call();

最终会调用具体任务类重写的 Execute()

例如,普通 SQL 使用 BasicStatementTask 表示:

复制代码
class BasicStatementTask : public SQLOperation
{
public:
    /*
     * sql:需要执行的SQL字符串
     * async:是否需要返回查询结果
     */
    BasicStatementTask(
        const char* sql,
        bool async = false
    );

    ~BasicStatementTask() override;

    // 执行当前SQL任务
    bool Execute() override;

    // 获取异步查询结果对应的future
    QueryResultFuture GetFuture() const
    {
        return m_result->get_future();
    }

private:
    const char* m_sql;             // 保存SQL字符串
    bool m_has_result;             // 是否需要返回结果
    QueryResultPromise* m_result;  // 用于传递异步查询结果
};

构造函数会复制 SQL 字符串:

复制代码
BasicStatementTask::BasicStatementTask(
    const char* sql,
    bool async)
    : m_result(nullptr)
{
    /*
     * 使用strdup复制SQL。
     *
     * 不能只保存调用者传入的指针,
     * 因为任务真正执行时,原字符串可能已经失效。
     */
    m_sql = strdup(sql);

    // async为true表示这是一个需要返回结果的异步查询
    m_has_result = async;

    if (async)
    {
        // 创建promise,后台线程通过它保存查询结果
        m_result = new QueryResultPromise();
    }
}

之所以不能直接这样保存:

复制代码
m_sql = sql;

是因为任务放入队列后,不一定立即执行。

例如:

复制代码
void SubmitTask()
{
    std::string sql = "UPDATE actor SET first_name='Tom'";

    SakilaDatabase.Execute(sql.c_str());

    // 函数结束后,sql对象被销毁
}

如果任务只保存 sql.c_str() 返回的地址,那么后台线程真正执行时,这个地址可能已经失效。

使用:

复制代码
m_sql = strdup(sql);

会为 SQL 单独申请一块内存并复制内容。

任务销毁时再释放:

复制代码
BasicStatementTask::~BasicStatementTask()
{
    // 释放strdup申请的SQL字符串
    free((void*)m_sql);

    // 如果创建了promise,同时释放promise对象
    if (m_has_result && m_result != nullptr)
    {
        delete m_result;
    }
}

任务的执行逻辑如下:

复制代码
bool BasicStatementTask::Execute()
{
    /*
     * 需要返回结果时,执行查询操作。
     */
    if (m_has_result)
    {
        ResultSet* result = m_conn->Query(m_sql);

        /*
         * 查询失败或结果为空,
         * 将一个空结果写入promise。
         */
        if (result == nullptr ||
            result->GetRowCount() == 0 ||
            !result->NextRow())
        {
            delete result;

            m_result->set_value(
                QueryResult(nullptr)
            );

            return false;
        }

        /*
         * 查询成功,将结果写入promise。
         *
         * 与该promise关联的future随后会变为就绪状态。
         */
        m_result->set_value(
            QueryResult(result)
        );

        return true;
    }

    /*
     * 不需要返回结果时,
     * 直接执行INSERT、UPDATE或DELETE等SQL。
     */
    return m_conn->Execute(m_sql);
}

因此,同一个 BasicStatementTask 可以表示两种任务:

复制代码
BasicStatementTask(sql, false)
        ↓
只执行SQL,不返回结果

BasicStatementTask(sql, true)
        ↓
执行查询,并通过promise返回结果

三、生产者---消费者队列如何保存任务

业务线程负责创建并提交任务,可以看作生产者。

数据库工作线程负责获取并执行任务,可以看作消费者。

因此,这里使用一个生产者---消费者队列:

复制代码
template <typename T>
class ProducerConsumerQueue
{
private:
    // 保护任务队列
    std::mutex _queueLock;

    // 保存等待执行的任务
    std::queue<T> _queue;

    // 控制工作线程等待和唤醒
    std::condition_variable _condition;

    // 队列是否已经停止运行
    std::atomic<bool> _shutdown;

public:
    ProducerConsumerQueue()
        : _shutdown(false)
    {
    }
};

1. 提交任务

任务入队使用 Push()

复制代码
void Push(const T& value)
{
    /*
     * 多个业务线程可能同时提交任务,
     * 因此修改队列前需要加锁。
     */
    std::lock_guard<std::mutex> lock(_queueLock);

    // 将任务放入队尾
    _queue.push(value);

    /*
     * 新任务到来后,
     * 唤醒一个正在等待的工作线程。
     */
    _condition.notify_one();
}

执行过程为:

复制代码
业务线程创建任务
        ↓
获取_queueLock
        ↓
任务进入队尾
        ↓
释放_queueLock
        ↓
notify_one唤醒一个工作线程

由于使用了:

复制代码
std::lock_guard<std::mutex>

Push() 函数结束时,互斥锁会自动释放,不需要手动调用 unlock()

2. 等待并获取任务

工作线程使用 WaitAndPop() 获取任务:

复制代码
void WaitAndPop(T& value)
{
    /*
     * unique_lock允许condition_variable
     * 在等待期间自动释放和重新获得互斥锁。
     */
    std::unique_lock<std::mutex> lock(_queueLock);

    /*
     * 队列为空并且没有关闭时,
     * 当前工作线程进入等待状态。
     */
    while (_queue.empty() && !_shutdown)
    {
        _condition.wait(lock);
    }

    /*
     * 队列关闭,或者被唤醒后仍然没有任务,
     * 直接返回。
     */
    if (_queue.empty() || _shutdown)
    {
        return;
    }

    // 取出队头任务
    value = _queue.front();

    // 从队列中删除该任务
    _queue.pop();
}

当队列为空时:

复制代码
_condition.wait(lock);

会完成两个操作:

复制代码
自动释放_queueLock
        ↓
让当前工作线程进入休眠

如果等待期间不释放互斥锁,生产者就无法进入 Push() 添加任务,程序会发生死锁。

生产者调用:

复制代码
_condition.notify_one();

以后,一个工作线程被唤醒。

被唤醒的线程会重新获得 _queueLock,然后继续检查队列状态。

这里使用 while 而不是 if

复制代码
while (_queue.empty() && !_shutdown)

是因为条件变量可能出现虚假唤醒。

即使线程醒来,也不代表队列中一定存在任务,因此必须重新检查条件。

3. 关闭任务队列

关闭队列时调用:

复制代码
void Cancel()
{
    std::unique_lock<std::mutex> lock(_queueLock);

    /*
     * 清理尚未被工作线程执行的任务。
     */
    while (!_queue.empty())
    {
        T& value = _queue.front();

        // 如果T是指针,则delete该任务对象
        DeleteQueuedObject(value);

        _queue.pop();
    }

    // 设置队列关闭标志
    _shutdown = true;

    // 唤醒所有正在等待任务的工作线程
    _condition.notify_all();
}

由于当前队列保存的是:

复制代码
SQLOperation*

所以队列关闭时,需要释放剩余任务:

复制代码
delete operation;

代码通过 std::enable_if 判断队列元素是否为指针:

复制代码
template <typename E = T>
typename std::enable_if<
    std::is_pointer<E>::value
>::type
DeleteQueuedObject(E& object)
{
    delete object;
}

如果队列保存的不是指针,则什么也不做:

复制代码
template <typename E = T>
typename std::enable_if<
    !std::is_pointer<E>::value
>::type
DeleteQueuedObject(const E&)
{
}

这意味着任务提交到队列后,任务对象的所有权也交给了队列和工作线程:

复制代码
任务尚未执行:
由队列负责管理

任务已经取出:
由工作线程负责管理

队列关闭:
队列删除尚未执行的任务

任务执行完成:
工作线程删除任务

因此,调用者将任务放入队列后,不能再手动 delete

复制代码
BasicStatementTask* task =
    new BasicStatementTask(sql);

Enqueue(task);

// 错误:任务所有权已经交给队列
// delete task;

四、DatabaseWorker 如何执行异步任务

每一个异步连接都会创建一个 DatabaseWorker

复制代码
class DatabaseWorker
{
public:
    DatabaseWorker(
        ProducerConsumerQueue<SQLOperation*>* queue,
        MySQLConnection* connection
    );

    ~DatabaseWorker();

private:
    // 所有异步工作线程共享的任务队列
    ProducerConsumerQueue<SQLOperation*>* _queue;

    // 当前工作线程专属的MySQL连接
    MySQLConnection* _connection;

    // 工作线程入口函数
    void WorkerThread();

    // 真正运行的C++线程
    std::thread _workerThread;

    // 控制工作线程退出
    std::atomic<bool> _cancelationToken;

    // 禁止复制DatabaseWorker
    DatabaseWorker(const DatabaseWorker&) = delete;
    DatabaseWorker& operator=(
        const DatabaseWorker&) = delete;
};

构造函数启动工作线程:

复制代码
DatabaseWorker::DatabaseWorker(
    ProducerConsumerQueue<SQLOperation*>* newQueue,
    MySQLConnection* connection)
{
    _connection = connection;
    _queue = newQueue;

    // 初始状态不退出
    _cancelationToken = false;

    /*
     * 创建后台线程。
     *
     * 线程从DatabaseWorker::WorkerThread开始运行,
     * this表示将当前对象传递给成员函数。
     */
    _workerThread = std::thread(
        &DatabaseWorker::WorkerThread,
        this
    );
}

工作线程的核心循环如下:

复制代码
void DatabaseWorker::WorkerThread()
{
    if (_queue == nullptr)
    {
        return;
    }

    for (;;)
    {
        SQLOperation* operation = nullptr;

        /*
         * 从共享任务队列获取一个任务。
         *
         * 队列为空时,当前工作线程会进入休眠,
         * 不会持续占用CPU。
         */
        _queue->WaitAndPop(operation);

        /*
         * 线程池正在关闭,或者没有取得任务,
         * 结束工作线程。
         */
        if (_cancelationToken || operation == nullptr)
        {
            return;
        }

        /*
         * 将当前工作线程专属的MySQL连接
         * 交给SQL任务。
         */
        operation->SetConnection(_connection);

        // 执行具体SQL任务
        operation->call();

        /*
         * 任务执行完成后释放任务对象。
         */
        delete operation;
    }
}

完整过程可以表示为:

复制代码
WaitAndPop等待任务
        ↓
取得一个SQLOperation
        ↓
SetConnection绑定数据库连接
        ↓
call调用任务
        ↓
Execute执行具体SQL
        ↓
delete释放任务
        ↓
继续等待下一个任务

工作线程不需要判断当前任务是普通 SQL、预处理语句还是事务。

这是因为所有任务都继承自:

复制代码
SQLOperation

并且都实现了自己的:

复制代码
Execute()

这种设计利用了 C++ 的多态。

工作线程只依赖统一接口:

复制代码
operation->call();

至于真正执行什么操作,由具体任务对象决定。

关闭工作线程时:

复制代码
DatabaseWorker::~DatabaseWorker()
{
    // 通知工作线程退出
    _cancelationToken = true;

    /*
     * 关闭共享任务队列,
     * 清理尚未执行的任务,并唤醒等待线程。
     */
    _queue->Cancel();

    /*
     * 等待后台线程彻底结束。
     *
     * 如果不join,DatabaseWorker对象销毁后,
     * 后台线程可能继续访问已经失效的this指针。
     */
    if (_workerThread.joinable())
    {
        _workerThread.join();
    }
}

这里的销毁顺序非常重要:

复制代码
设置退出标志
        ↓
关闭任务队列
        ↓
唤醒等待线程
        ↓
工作线程退出
        ↓
join等待线程结束
        ↓
DatabaseWorker完成销毁

如果只设置:

复制代码
_cancelationToken = true;

但工作线程正在:

复制代码
_queue->WaitAndPop(operation);

中休眠,它可能无法及时醒来。

因此,还需要:

复制代码
_queue->Cancel();

通过 notify_all() 唤醒等待线程。


五、异步 Execute 和 AsyncQuery 的完整流程

连接池通过 Enqueue() 统一提交任务:

复制代码
template <class T>
void DatabaseWorkerPool<T>::Enqueue(
    SQLOperation* operation)
{
    // 将SQL任务放入共享任务队列
    _queue->Push(operation);
}

1. 不需要返回结果的异步操作

例如异步执行 INSERTUPDATEDELETE

复制代码
template <class T>
void DatabaseWorkerPool<T>::Execute(
    const char* sql)
{
    // SQL为空时不执行
    if (Trinity::IsFormatEmptyOrNull(sql))
    {
        return;
    }

    /*
     * 创建一个不返回查询结果的SQL任务。
     *
     * BasicStatementTask的async参数默认为false。
     */
    BasicStatementTask* task =
        new BasicStatementTask(sql);

    // 将任务交给异步工作线程
    Enqueue(task);
}

调用方式如下:

复制代码
SakilaDatabase.Execute(
    "UPDATE actor "
    "SET first_name='Tom' "
    "WHERE actor_id=1"
);

调用 Execute() 后,当前业务线程只负责把任务放入队列,不等待 SQL 执行完成。

运行过程为:

复制代码
业务线程调用Execute
        ↓
创建BasicStatementTask
        ↓
任务进入队列
        ↓
Execute立即返回
        ↓
某个DatabaseWorker取得任务
        ↓
通过自己的MySQL连接执行SQL
        ↓
删除任务对象

2. 需要查询结果的异步操作

需要返回结果时,使用:

复制代码
template <class T>
QueryCallback DatabaseWorkerPool<T>::AsyncQuery(
    const char* sql)
{
    /*
     * 第二个参数为true,
     * 表示该任务需要返回查询结果。
     */
    BasicStatementTask* task =
        new BasicStatementTask(sql, true);

    /*
     * 必须先取得future,再将任务放入队列。
     *
     * 任务入队后可能立即被工作线程执行并删除,
     * 如果先入队再调用GetFuture,
     * task指针可能已经失效。
     */
    QueryResultFuture result =
        task->GetFuture();

    // 将任务交给后台工作线程
    Enqueue(task);

    /*
     * 使用QueryCallback封装future,
     * 上层代码可以在查询完成后处理结果。
     */
    return QueryCallback(
        std::move(result)
    );
}

这里最值得注意的是执行顺序:

复制代码
QueryResultFuture result = task->GetFuture();
Enqueue(task);

不能轻易写成:

复制代码
Enqueue(task);

// 危险:任务可能已经执行完成并被删除
QueryResultFuture result = task->GetFuture();

因为异步工作线程可能非常快。

任务一旦进入队列,就可能立即被取走、执行并删除。因此,必须在移交任务所有权之前取得 future

异步查询的使用示例如下:

复制代码
#include "AsyncCallbackProcessor.h"
#include <atomic>
#include <chrono>
#include <thread>

int main()
{
    MySQL::Library_Init();

    DatabaseLoader loader;

    loader.AddDatabase(
        SakilaDatabase,
        "127.0.0.1;3306;root;123456;sakila",
        8,
        2
    );

    if (!loader.Load())
    {
        MySQL::Library_End();
        return 1;
    }

    // 用于保存并处理尚未完成的异步回调
    AsyncCallbackProcessor<QueryCallback> processor;

    // 标记当前异步查询是否已经处理完成
    std::atomic<bool> finished = false;

    /*
     * 提交异步查询,并注册结果处理函数。
     */
    processor.AddCallback(
        SakilaDatabase.AsyncQuery(
            "SELECT actor_id, first_name, last_name "
            "FROM actor WHERE actor_id=1"
        ).WithCallback(
            [&finished](QueryResult result)
            {
                if (!result)
                {
                    TC_LOG_ERROR(
                        "",
                        "异步查询没有返回数据"
                    );

                    finished = true;
                    return;
                }

                TC_LOG_INFO(
                    "",
                    "actor_id=%u, name=%s %s",
                    (*result)[0].GetUInt32(),
                    (*result)[1].GetString(),
                    (*result)[2].GetString()
                );

                finished = true;
            }
        )
    );

    /*
     * AsyncQuery只负责让后台线程执行SQL。
     *
     * 回调不会自动在当前线程中执行,
     * 需要定期检查future是否已经就绪。
     */
    while (!finished)
    {
        processor.ProcessReadyCallbacks();

        // 避免循环检查占满CPU
        std::this_thread::sleep_for(
            std::chrono::milliseconds(20)
        );
    }

    SakilaDatabase.Close();
    MySQL::Library_End();

    return 0;
}

整个异步查询流程如下:

复制代码
业务线程调用AsyncQuery
        ↓
创建BasicStatementTask
        ↓
从promise取得future
        ↓
任务进入生产者---消费者队列
        ↓
AsyncQuery返回QueryCallback
        ↓
后台DatabaseWorker获取任务
        ↓
为任务绑定MySQL连接
        ↓
执行Query
        ↓
通过promise保存查询结果
        ↓
future变为ready状态
        ↓
ProcessReadyCallbacks检测到结果
        ↓
执行用户注册的回调函数

使用异步连接池时,需要注意以下问题:

复制代码
1. Execute只是将任务放入队列,不代表SQL已经执行完成;

2. 异步任务提交后,不能再手动删除任务对象;

3. 工作线程执行完成后会自动delete任务;

4. 队列关闭时,尚未执行的任务会被直接清理;

5. AsyncQuery需要先取得future,再将任务放入队列;

6. 回调函数不会自动执行,需要定期调用
   ProcessReadyCallbacks();

7. 关闭连接池前,应停止提交新的异步任务;

8. 当前Cancel属于取消式关闭,不能保证队列中
   尚未执行的SQL全部完成。

异步数据库执行机制的核心可以总结为:

复制代码
业务线程只负责创建并提交SQL任务;
所有异步连接共享一个线程安全任务队列;
每个异步连接拥有一个专属DatabaseWorker;
工作线程从队列中竞争任务并绑定自己的连接;
任务通过多态调用各自的Execute函数;
无结果任务执行后直接结束;
有结果任务通过promise和future传递查询结果。

0voice · GitHub

相关推荐
yyds_yyd_100861 小时前
877. 石子游戏(2026.08.02)& 486. 预测赢家(2026.08.01)
c++·leetcode
小小帅呀2 小时前
CUDA编程实战12:原子操作与高性能直方图——从正确累加到低冲突并行更新
c++·人工智能·线性代数·矩阵
加油码2 小时前
共享内存详解:原理、系统调用与高性能进程间通信
linux·c语言·c++
Lzh编程小栈2 小时前
【STM32底层精讲】RCC时钟系统超全详解(时钟树+源码+避坑指南)
c语言·stm32·单片机·嵌入式硬件·面试
June`3 小时前
常量内存和只读缓存
c++·人工智能·算法·cuda
Cx330❀3 小时前
【Linux网络】深入 HTTP 协议(五):从 Cookie/Session 原理到 C++ 源码实战
linux·运维·服务器·开发语言·网络·c++·http
鸿芯微控科技3 小时前
MFC气体转换系数怎么用?标定气体、换算方向与Python批量计算
c++·python·mfc
hPw0eKIqD12 小时前
C++ 模板参数推导问题小记(非推导上下文)
开发语言·c++
程序员爱德华12 小时前
Python与C++:异同点对比
c++·python