Заморозка интерфейса Qt при использовании API PostgreSQL
Прошу помощи в диагностике проблемы:
Есть проект Qt 5.14.2 который развивается в мультиплатформенном варианте. Работает с базой данных PostgreSQL (от 9.x до 15.x). Непосредственная разработка ведётся в Win 10 x64 с компилятором MSVC 2022 x64 и тестируется по Linux x64. В рамках проекта не используется встроенный плагин Qt pgsql (причины есть - но они не столь важны). Вместо него пишется набор своих интерфейсных классов, которые взаимодействуют с Postgre через libpq, что позволяет осуществлять полный контроль над БД через API.
Возникла задача: грузить результаты одной view в фоновом режиме. Количество записей в запросе достигает ~50 тыс. и всё время растёт, хотя и не очень быстро. Был реализован дополнительный поток 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();
}
при окончании (после emit 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". В последнем коде вызов функции обязателен перед завершением запроса для устранения утечки памяти - это требование API PostgreSQL.
Умом я понимаю, что очистка блока памяти моего размера, в принципе не быстрое дело, но: всё выполняется в отдельном потоке а замораживается интерфейс всей программы.
Первая мысль: сделать (и сделано) объект типа DataReader с чтением по строкам, но он работает в разы медленнее, поскольку такая функция очистки вызывается для каждой полученной записи с сервера (при использовании построчного режима).
Вторая мысль: Объекты TConnector / TQuery - простые классы, не наследники от QObject. Пока не проверял.
В любом случае, возникает вопрос: при работе с сервером в ОТДЕЛЬНОМ ПОТОКЕ вызов его API функции вызывает фриз интерфейса всей программы. Почему?
UPD 1: В C# есть NpgSQL, работающий с PostgreSQL напрямую. У него есть DataReader, с которым не возникает таких сложностей в другом проекте, подключённом к этой же БД. Повторить тут не смог.
Прошу поделиться мыслями.