虽说Qt多线程的使用方法在官方文档中已经写的很清楚了,但想要用好还是要费一些脑筋的。
今天我们使用多线程的方式来执行sqlite语句,这种需求在实际项目中非常常见,毕竟把IO操作放到主线程中执行是一件很不明智的事情。
子线程的执行范围
首先我们先问自己一个问题,执行一个查询语句,再将查询结果更新到界面中,哪一步要放到主线程,哪一步要放到其他的线程,我们先写一个简单的代码。
//子线程
QSqlQuery query("SELECT name FROM users WHERE id = 1");
query.exec();
query.next();
QString name = query.value(0).toString();
//主线程
label->setText("用户姓名: " + name);
通过这段代码,我们其实可以清晰地分辨出子线程的执行范围,判断方法也很简单,就是不要在子线程执行ui操作。
如何进行多线程通信
解决了子线程执行范围的问题,我们又遇到了问题,就是Qt线程通信的问题,Qt的线程间通信官方推荐的做法是使用信号槽,如果我们为每一条查询语句制定一个信号槽,查询返回的结果也制定一个信号槽,查询语句少还好说,查询多了简直就是工程的灾难,大量的信号槽需要我们维护,这不是我们想要的。
那我们能不能所有查询都使用一个信号槽,答案是可以的。
在执行查询时,我们一般需要两个参数,一是查询所需的语句,二是查询所需的参数,只要有这两个参数,我们就可以执行查询,所以我们规定槽函数如下。
void exec(QString const & sql,QSharedPointer<QVariantMap> values);
第一个参数就是要执行的sql语句,第二个是所需要的参数,我们所有语句的执行都是使用bindValue的形式进行传参,这是一个很好的习惯,至于为什么使用智能指针,因为这涉及到多线程析构的问题,所以我们尽量不要手动管理内存的生命周期。
查询结果序列化
解决了多线程通信的问题,我们又遇到了一个问题,就是在哪里进行查询结果的序列化,我们显然不能查询完直接就写序列化代码,这不符合模块化的原则。但序列化的任务需要在子线程执行。
这里我们需要使用虚函数,我们先将执行步骤定义下来。
class QSqlQuery;
class ThreadSqlQueryInterface
{
public:
virtual ~ThreadSqlQueryInterface() {}
virtual void parseQuery(QSqlQuery & query)=0;
virtual void dealQuery()=0;
};
parseQuery(QSqlQuery & query) 代表着查询结果序列化的行为,dealQuery() 代表将序列化的结果进一步处理的行为。
我们将这个接口传递给子线程,子线程调用 parseQuery(QSqlQuery & query) 这个钩子函数,调用完成再将接口传回到主线程,由主线程调用 dealQuery() 函数。
void exec(QString const & sql,QSharedPointer<QVariantMap> values,QSharedPointer<ThreadSqlQueryInterface> interface);
这样我们执行一条sql语句需要传递三个参数。
整活时刻
解决了上述问题,整个代码基本上就完成了,但拿破仑曾经说过,不想整活的CPP程序员不是好程序员。CPP程序员怎么整活,那自然要搬出我们的亲爱的模板功能。
首先我不想让我的类都继承ThreadSqlQueryInterface这个接口,但同时我想在执行dealQuery() 时捕获其他界面元素,这时能解决这个需求的方式只能是模板加Lambda了,不废话了,直接上代码。
template<typename T>
class ThreadSqlQuery : public ThreadSqlQueryInterface
{
public:
inline void parseQuery(QSqlQuery & query)override final{
if(parseQueryFunction){
parseQueryFunction(query,collection);
}
};
inline void dealQuery()override final{
if(dealEntityFunction){
dealEntityFunction(collection);
}
};
T collection;
std::function<void(T const &)> dealEntityFunction;
std::function<void(QSqlQuery &,T &)> parseQueryFunction;
};
有一说一这段代码其实挺简单的,那么这么写的好处是什么,直接上代码。
#include "threaddblib/ThreadSqlQuery.h"
#include "Student.h"
#include <QList>
class StudentQuery
{
public:
StudentQuery();
void static parseQueryFunction(QSqlQuery & query,QList<Student> & students);
public:
void getAllStudent(std::function<void(QList<Student>const&)> const & fn);
void insertStudent(Student const & student);
private:
ThreadSqlQuery<QList<Student>> query;
};
StudentQuery::StudentQuery()
{
GlobleApp::getInstance().threadSqlite->exec(" CREATE TABLE IF NOT EXISTS Student ( "
" id INTEGER PRIMARY KEY, "
" name TEXT NOT NULL, "
" age INTEGER NOT NULL, "
" grade TEXT NOT NULL) ");
}
void StudentQuery::parseQueryFunction(QSqlQuery &query, QList<Student> &students)
{
while(query.next()){
students.append(Student(query.value(0).toInt(),
query.value(1).toString(),
query.value(2).toInt(),
query.value(3).toString()));
}
}
void StudentQuery::getAllStudent(const std::function<void (const QList<Student> &)> &fn)
{
QSharedPointer<ThreadSqlQuery<QList<Student>>> query = QSharedPointer<ThreadSqlQuery<QList<Student>>>(new ThreadSqlQuery<QList<Student>>());
query->parseQueryFunction = StudentQuery::parseQueryFunction;
query->dealEntityFunction = fn;
GlobleApp::getInstance().threadSqlite->exec(" SELECT * FROM Student ",QSharedPointer<QVariantMap>(),query);
}
void StudentQuery::insertStudent(const Student &student)
{
QSharedPointer<QVariantMap> vars = QSharedPointer<QVariantMap>(new QVariantMap());
vars->insert(":name",student.getName());
vars->insert(":age",student.getAge());
vars->insert(":grade",student.getGrade());
GlobleApp::getInstance().threadSqlite->exec(" INSERT INTO Student (name, age, grade) VALUES (:name, :age, :grade) ",vars);
}
好处显而易见,我们舍弃了继承的方式,这样降低了耦合度,同时在执行getAllStudent(const std::function<void (const QList &)> &fn)函数时,我们可以使用Lambda捕获变量的功能轻松捕获到界面元素,无需直接传递UI元素,降低了依赖。
代码已上传至github。