我需要帮助诊断问题:
有一个项目 Qt 5.14.2,它是在多平台变体中开发的。适用于 PostgreSQL 数据库(从 9.x 到 15.x)。使用MSVC 2022 x64编译器在Win 10 x64中进行直接开发,并在Linux x64上进行测试。该项目不使用内置的 Qt pgsql 插件(有原因 - 但它们并不那么重要)。相反,编写了一组自己的接口类,通过 libpq 与 Postgre 交互,这允许您通过 API 对数据库进行完全控制。
有一项任务:在后台加载一个视图的结果。查询中的记录数量达到约 5 万条,并且一直在增长,尽管速度不是很快。实现了一个附加线程 QThread,它与程序并行加载必要的内容。
问题:发生 2 次冻结:
- 第一,当线程中请求数据时,这是可以理解的,本质上不会影响任何事情;
- 第二个 - 在下载过程结束时,现在它将程序界面冻结。
问题在于第二次冻结,或者更确切地说,它冻结了整个程序的界面,尽管纯粹从技术上来说这是可以理解的。
那么详细信息:
- 正如已经说过的,有自己的与 PostgreSQL 交互的对象,它们可以根据我的需要工作。如果代码很重要,我稍后会给出。
- 使用一个对象创建 QThread 来提供负载:
void Model::LoadData()
{
//-- запускаем поток, загрузки данных
load_thread = new QThread(); //-- переменная определена в классе
load_thread->setObjectName("BW_load");
mtLoader = new ubiVulLoader(&DataList);
mtLoader->moveToThread(load_thread);
connect(mtLoader, SIGNAL(setProgressMax(int)), this, SLOT(setProgressMax(int)));
connect(mtLoader, SIGNAL(setProgress(int)), this, SLOT(setProgress(int)), Qt::DirectConnection);
connect(mtLoader, SIGNAL(endProcess()), this, SLOT(endProcess()));
connect(load_thread, SIGNAL(started()), mtLoader, SLOT(doLoad()));
load_thread->start();
}
最后(在发出 endProcess 之后),处理以下代码:
void Model::endProcess()
{
emit sendProcessEnd();
disconnect(mtLoader, SIGNAL(setProgressMax(int)), this, SLOT(setProgressMax(int)));
disconnect(mtLoader, SIGNAL(setProgress(int)), this, SLOT(setProgress(int)));
disconnect(mtLoader, SIGNAL(endProcess()), this, SLOT(endProcess()));
load_thread->quit();
load_thread->wait(); //-- или deleteLater() - без разницы
delete mtLoader;
delete load_thread;
}
doLoad() 函数本身(缩写)如下所示:
void mtLoader::doLoad()
{
auto con = new TConnector();
if (con->Open())
{
if (con->transactionStart())
{
//-- ВСЕ ДЕЙСТВИЯ ВЫПОЛНЯЮТСЯ В РАМКАХ ОДНОЙ (!!!) ТРАНЗАКЦИИ
/*
Использование функций доступа по номеру поля а не по его имени существенно ускоряет
обработку. */
QString q_load =
QString("select " ... тут много полей по порядку
" from table ...");
TQuery *qc = con->CreateNewQuery("select count(*) from table");
long cnt = 0;
if (qc->Execute())
{
cnt = qc->FieldByNum(0).toLongLong();
}
delete qc;
if (cnt > 0)
{
emit setProgressMax(cnt); //-- оповещение о максимуме
recs->resize(cnt);
//-- чтобы не перегружать событиями основной интерфейс искусственно снизим частоту оповещения
//-- см. в конец цикла
auto step = cnt / 100;
auto st = 0;
int progress = 0;
TQuery *q = con->CreateNewQuery(q_load);
if (q->Execute())
{
int cnt = q->getRowCount();
TData *rec = nullptr;
for (int i = 0; i < cnt; i++)
{
recs->data()[i] = new TData();
rec = recs->at(i);
//-- служебные поля таблицы
rec->id = q->FieldByNum(0).toLongLong();
rec->code = q->FieldByNum(2).toString();
rec->name = q->FieldByNum(1).toString();
... ну и так далее заполняем объект
//--
q->Next();
//-- чтобы не перегружать событиями основной интерфейс искусственно снизим частоту оповещения
//-- оповещение о прогрессе будем отсылать 1 раз за 1 процент выполнения, а не на каждую запись
st += 1;
progress+=1;
if (st > step)
{
st = 0;
emit setProgress(progress); //-- оповещение о прогрессе выполнения
}
}
emit setProgress(progress); //-- оповещение о прогрессе выполнения
}
else
{
//-- в случае ошибки ...
con->transactionRollback();
con->Close();
delete q;
delete con;
return;
}
delete q; //-- <<<< FREEZE
}
}
con->transactionRollback();
con->Close();
}
delete con;
emit endProcess(); //-- оповещение об окончании процесса
}
第二次冻结发生在对象 q 被删除时,在此期间执行以下代码:
...
if (_res!=nullptr)
{
PQclear(_res); //-- <<<< FREEZE
_res=nullptr;
}
...
冰点标记为“FREEZE”。在最后的代码中,在终止查询之前需要调用函数来修复内存泄漏 - 这是 PostgreSQL API 的要求。
从理智上来说,我明白清除一个我大小的内存块原则上不是一件很快的事情,但是:一切都是在单独的线程中完成的,整个程序的界面被冻结。
第一个想法:制作(并完成)一个 DataReader 类型的对象,逐行读取,但它的工作速度要慢很多,因为从服务器接收到的每个记录都会调用这样的清理函数(当使用行模式时)。
第二个想法:TConnector / TQuery 对象是简单的类,而不是 QObject 的后代。还没检查过。
无论如何,问题出现了:当在单独的线程中使用服务器时,调用其 API 函数会导致整个程序的界面冻结。为什么?
UPD 1:C# 具有可直接与 PostgreSQL 配合使用的 NpgSQL。他有一个DataReader,使用它在连接到同一数据库的另一个项目中就不存在这样的困难。这里无法重复。
请分享您的想法。
所描述的问题消失了,只需在不调试的情况下构建项目,而是使用发布程序集即可。我决定取消订阅,你永远不知道谁会遇到类似的...:)