Форум программистов, компьютерный форум, киберфорум
icpu
Войти
Регистрация
Восстановить пароль
Оценить эту запись

QThread + QtSql: Асинхронные запросы

Запись от icpu размещена 21.12.2015 в 14:13
Обновил(-а) icpu 20.01.2016 в 07:39

Всем добрых суток времени!

Думаю, подавляющее большинство Qt разработчиков, трогавших работу с базами данных, знают, что QtSql создан однопоточным. Даже более того, соединения с базой данных нельзя передавать между потоками, нельзя передавать и курсоры, и запросы, и, вообще, всё. Почти столь же подавляющее большинство встречало вторую проблему - отсутствие стоянок "Free-Bike" в радиусе мили от поискового запроса, как же эти потоки добавить. Пора ударить коммунистической рукой по классовой несправедливости!

УВАГА: Перед вами настоящий велосипед из настоящих костылей без применения синей изоленты! Используйте на свой страх и риск!
tl;dr

C++ (Qt)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
typedef QVector< QVariant > VariantVector;
typedef QVector< VariantVector > VariantVector2;
 
class Worker : public QObject {
    Q_OBJECT
public:
    Worker();
    virtual ~Worker();
    bool init();
    bool isInitiated();
    int queryNum();
 
    static int exec(Worker * w, QString query, QStringList bindNames = QStringList(), VariantVector bindList = VariantVector());
 
private slots:
    void slotExec(int num, QString query, QStringList bindNames, VariantVector bindList);
 
signals:
    void signalExec(int, VariantVector2);
    void sqlError(int, QSqlError)
 
protected:
    QThread * th;
    QSqlDatabase db;
};
C++ (Qt)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
QString GenConnectionName() {
    static int num = 0;
    return QString("DBConnection%1").arg(num++);
}
 
Worker::Worker(): QObject() {
    th = new QThread();
    qRegisterMetaType<VariantVector>("VariantVector");
    qRegisterMetaType<VariantVector2>("VariantVector2");
}
Worker::~Worker() {
    db.close();
    th->quit();
    th->deleteLater();
}
bool Worker::init() {
    if (db.isOpen())  return true;
    if (!th->isRunning()) {
        th->start(QThread::LowPriority);
        moveToThread(th);
    }
    if (!db.isOpen()) {
        db = QSqlDatabase::addDatabase("QPSQL",GenConnectionName());
        db.setHostName("localhost");
        db.setPort(5432);
        db.setUserName("postgres");
        db.setDatabaseName("postgres");
        QTimer timer;
        timer.start(10000);
        forever {
            if (db.isOpen()) break;
            if (db.open()) break;
            if (!timer.remainingTime()) break;
    }    }
    if (!db.isOpen()) return false;
    return true;
}
 
bool Worker::isInitiated() {
    return th->isRunning() && db.isOpen();
}
int Worker::queryNum() { // Эта штука может сбоить без мутексов
    static int i = 0; 
    return i++; 
} 
int Worker::exec(Worker*w, QString q, QStringList s, VariantVector v){
    int i = queryNum();
    QMetaObject::invokeMethod(w, "slotExec", Qt::QueuedConnection, Q_ARG(int,i), Q_ARG(QString,q), Q_ARG(QStringList,s), Q_ARG(VariantVector,v))
    return i;
}
void Worker::slotExec(int num, QString query, QStringList bindNames, VariantVector bindList)
{
    QSqlQuery q(db);
    q.prepare(query);
    for (int i = 0; i < bindNames.size(); ++i)
        q.bindValue(bindNames[i],bindList[i]);
    q.exec();
    if (q.lastError().isValid())
         emit sqlError(num,q.lastError());
    VariantVector2 vv;
    while(q.next()) {
         static int j = -1; ++j;
         vv.push_back(VariantVector());
         for (int i = 0; q.value(i).isValid(); ++i)
             vv[j].push_back(q.value(i));
    }
    emit signalExec(num,vv);

Итак, как вынести соединение с базой в другой поток? Как и всё остальное, через QObject
C++ (Qt)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// *.H
class Worker : public QObject {
    Q_OBJECT
public:
    Worker();
    virtual ~Worker();
    bool init();
    bool isInitiated();
protected:
    QThread * th;
    QSqlDatabase db;
};
 
 
// *.CPP
QString GenConnectionName() {
    static int num = 0;
    return QString("DBConnection%1").arg(num++);
}
Worker::Worker(): QObject() {
    th = new QThread();
}
Worker::~Worker() {
    db.close();
    th->quit();
    th->deleteLater();
}
bool Worker::init() {
    if (db.isOpen())  return true;
    if (!th->isRunning()) {
        th->start(QThread::LowPriority);
        moveToThread(th);
    }
    if (!db.isOpen()) {
        db = QSqlDatabase::addDatabase("QPSQL",GenConnectionName());
        db.setHostName("localhost");
        db.setPort(5432);
        db.setUserName("postgres");
        db.setDatabaseName("postgres");
        QTimer timer;
        timer.start(10000);
        forever {
            if (db.isOpen()) break;
            if (db.open()) break;
            if (!timer.remainingTime()) break;
    }    }
    if (!db.isOpen()) return false;
    return true;
}
 
bool Worker::isInitiated() {
    return th->isRunning() && db.isOpen();
}
Отлично, БД вынесена в отдельный поток. Но какой в этом толк, если всё равно нужно ждать запросы? Никакого. Значит, нужно добавить очередь запросов и асинхронное возвращение результатов. Всё это очень просто реализуется через слоты.

Добавим поддержку запросов:
C++ (Qt)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// *.H
typedef QVector< QVariant > VariantVector;
typedef QVector< VariantVector > VariantVector2;
 
class Worker : public QObject {
    int queryNum() { static int i = 0; return i++; } // Эта штука может сбоить без мутексов
   
    // ...
    static int exec(Worker * w, QString query, QStringList bindNames = QStringList(), VariantVector bindList = VariantVector());
private slots:
    void slotExec(int num, QString query, QStringList bindNames, VariantVector bindList);
signals:
    void signalExec(int, VariantVector2);
    void sqlError(int, QSqlError)
//...
};
 
// *.CPP
int Worker::exec(Worker*w, QString q, QStringList s, VariantVector v)
{
    int i = queryNum();
    QMetaObject::invokeMethod(w, "slotExec", Qt::QueuedConnection, Q_ARG(int,i), Q_ARG(QString,q), Q_ARG(QStringList,s), Q_ARG(VariantVector,v))
    return i;
}
void Worker::slotExec(int num, QString query, QStringList bindNames, VariantVector bindList)
{
    QSqlQuery q(db);
    q.prepare(query);
    for (int i = 0; i < bindNames.size(); ++i)
        q.bindValue(bindNames[i],bindList[i]);
    q.exec();
    if (q.lastError().isValid())
         emit sqlError(num,q.lastError());
    VariantVector2 vv;
    while(q.next()) {
         static int j = -1; ++j;
         vv.push_back(VariantVector());
         for (int i = 0; q.value(i).isValid(); ++i)
             vv[j].push_back(q.value(i));
    }
    emit signalExec(num,vv);
}
Что мы сделали? Воспользовались механизмом сигналов-слотов. Как гласит документация, при использовании слотов между потоками они преобразуются в сообщения. Однако нам пришлось сгородить небольшой костыль. Дело в том, что слоты в понимании Qt - обыкновенные функции, которые, при обычном синтаксисе, будут вызываться "в основном потоке", а точнее, блокировать оба потока до завершения работы. И потому [Боромир.gif] нельзя просто взять и вызвать слот. Затем и нужен весь этот ужас с QMetaObject::invokeMethod. Именно он поставит сообщение в очередь, не блокируя вызывающий поток.

Почти всё. Осталась одна деталь, нужно вызвать функцию инициализации метатипов:
C++ (Qt)
1
2
3
4
5
Worker::Worker(): QObject() {
    th = new QThread();
    qRegisterMetaType<VariantVector>("VariantVector");
    qRegisterMetaType<VariantVector2>("VariantVector2");
}
Без этого вызова попытки соединиться будут отклоняться с жалобами на неизвестный тип. Вызывать можно практически в любой части кода, но не стоит этим злоупотреблять. Всё-таки moc очень неповоротлив.

В принципе, всё, далее этот класс можно использовать следующим образом:
C++ (Qt)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// public slots:
void onReturnQuery(int i, VariantVector2 v) {
    if (id != i)
    return;
    foreach(auto i, v);
         qDebug() << i[0] << i[1];
}
 
void call() {
    Worker w;
    w.init();
    connect(&w,SIGNAL(signalExec(int,VariantVector2)),SLOT(onReturnQuery(int,VariantVector2)));
    id = Worker::exec("select 1,2 from 1;");
}
Или не таким дурацким. Выбор за вами.

Ну вот и всё, асинхронные многопоточные запросы реализованы. При желании таких рабочих можно объединить в пулы, реализовать в них больше от QSqlResult, но это уже другая история.

Не по теме:

Обо всех замечаниях по тексту и коду - кричите, как резанные.



Upd1: В статье был ряд опечаток и неточностей. В частности, класс в ряде мест назывался lpWorker, в одном из конструкторов ошибочно передавался this в QThread. Спасибо, Avazart, я верю в тебя, даже если ты не веришь в меня.
Размещено в Qt и иже с ним
Просмотров 1492 Комментарии 11
Всего комментариев 11
Комментарии
  1. Старый комментарий
    Аватар для Avazart
    C++ (Qt)
    1
    2
    3
    4
    
    class Worker : public QObject {
        Q_OBJECT
    public:
        lpWorker();
    Так Worker или lpWorker ?
    И почему Worker ?

    Как по мне код- фигня, лучше бы не выкладывал.
    Запись от Avazart размещена 14.01.2016 в 12:50 Avazart на форуме
    Обновил(-а) Avazart 14.01.2016 в 12:56
  2. Старый комментарий
    Аватар для icpu
    Цитата:
    Сообщение от Avazart Просмотреть комментарий
    Так Worker или lpWorker ?
    И почему Worker ?
    Исправил. Worker потому что во мне погиб сочинитель имён классов. В зародыше.
    Цитата:
    Сообщение от Avazart Просмотреть комментарий
    Как по мне код- фигня, лучше бы не выкладывал.
    А есть альтернативы? Вот я за час в обнимку с гуглом не нашёл. Я бы и не выкладывал, если бы вы, к примеру, написали в бложике чего-нибудь на тему. Но, увы, вы делали интересный и, одновременно, избыточный инструмент, который запросто заменяется DependencyWalker'ом.
    Запись от icpu размещена 15.01.2016 в 06:32 icpu вне форума
  3. Старый комментарий
    Аватар для Avazart
    Цитата:
    если бы вы, к примеру, написали в бложике чего-нибудь на тему.
    Я не сталкивался и не разбирался с данной темой, по этому не вижу смысла писать на это тему.
    Но для того что бы оценить ваш код этого не нужно.

    Цитата:
    заменяется DependencyWalker'ом.
    DependencyWalker'ом не определяет динамические зависимости(плагины) и не копирует файлы в соответствии с потребностями(расположение файлов) Qt.

    Цитата:
    Worker потому что во мне погиб сочинитель имён классов. В зародыше.
    А с ним программист?
    Worker обычно класс который передается в поток, а у вас непонятно что, даже не Controler

    Цитата:
    А есть альтернативы?
    Альтернатива почитать про потоки и писать по нормальному и правильно, а не тяп-ляп и еще и выкладывать.

    За один только
    C++ (Qt)
    1
    
    moveToThread(th);
    можно ...

    А ведь есть еще:
    C++ (Qt)
    1
    2
    3
    4
    5
    
    Worker::~Worker() {
        th->terminate();
        db.close();
        th->deleteLater();
    }
    Запись от Avazart размещена 15.01.2016 в 11:29 Avazart на форуме
    Обновил(-а) Avazart 15.01.2016 в 11:39
  4. Старый комментарий
    Аватар для icpu
    Цитата:
    DependencyWalker и иже с ними
    Хорошо, молодец, с меня печенька. Но вот QtSQL'у от этого ни холодно, ни жарко.
    Цитата:
    Worker обычно класс который передается в поток, а у вас непонятно что, даже не Controler
    Извините, что задел ваши религиозные чувства, но я агностик, и мне на всё это поровну. Захочу, в fAa переименую, а методы в _0(...); _1(...);. Моё право. А ваше право - плеваться и выть.
    Цитата:
    можно ...
    Чем moveToThread(th); не угодил? Утечек не создаёт, контроль над объектом не утекает. Как же сделать иначе, унаследовать QThread? Тогда прошу ознакомиться с холиваром, а не на потоки кивать.

    Ещё замечания?

    UPD: Worker по логике работы класса скорее Executor. Получает задание через стандартную очередь и выполняет его. Но был он назван Worker'ом, так теперь и есть.
    Запись от icpu размещена 15.01.2016 в 11:50 icpu вне форума
    Обновил(-а) icpu 15.01.2016 в 11:57
  5. Старый комментарий
    Аватар для Avazart
    Цитата:
    Моё право. А ваше право - плеваться и выть.
    Моё право отвернуться и уйти оставить вас наедине с вашим быдлокодом что бы вы сколь угодно долго на него плевали и выли...
    Цитата:
    Извините, что задел ваши религиозные чувства, но я агностик,
    Это не религия, это культура, хороший тон, то когда с вами и вашим кодом вежливо обращаются не зависимо от вероисповедания, а не придают забвению.
    Цитата:
    Чем moveToThread(th); не угодил?
    Ну погуглите тема избитая. Я же говорю прежде чем постить стоило хотя бы чуть почитать про потоки.
    Цитата:
    Тогда прошу ознакомиться с холиваром, а не на потоки кивать.
    Ознакомьтесь с примерами из официальной документацией и вопросы отпадут сами собой.
    Для использования годится как наследование(в зависимости от ситуации конечно), так и передача "рабочего" объекта в поток, но явно не то ... что у вас.
    Кстати между прочем кроме QThread есть еще и модуль QConcurent.

    Читайте последние сообщение в той теме:
    Цитата:
    Сообщение от Dmitriy_M Посмотреть сообщение
    По поводу moveToThread, то есть классы, экземпляры которых могут использоваться только в том потоке, в котором он создан.
    Классы для работы с БД насколько я знаю именно такие.
    Запись от Avazart размещена 15.01.2016 в 13:59 Avazart на форуме
    Обновил(-а) Avazart 15.01.2016 в 14:29
  6. Старый комментарий
    Аватар для icpu
    Цитата:
    Это не религия, это культура
    Выберите название для этого класса, будет оно. Без шуток.
    Цитата:
    Ну погуглите тема избитая. Я же говорю прежде чем постить стоило хотя бы чуть почитать про потоки.
    Я-то в курсе, быть может вы не очень? Или невнимательно просмотрели код?
    Ознакомьтесь с примерами из официальной документацией, в которой написано, что годится передача "рабочего" объекта в поток, как у меня и делается. Нэ?
    модуль QConcurent перемещает параметры-объекты в создаваемые им потоки (или передаваемый ему пул потоков), хотя классы БД могут использоваться только в том потоке, в котором они созданы. См. Worker::init()

    Пожалуйста, проверьте работоспособность класса. Вдруг окажется. что это быдлокожий уродец работает, и не создаёт при этом лишних сущностей.
    Запись от icpu размещена 15.01.2016 в 17:03 icpu вне форума
  7. Старый комментарий
    Аватар для Avazart
    Цитата:
    Выберите название для этого класса, будет оно. Без шуток.
    Пока претендует только на нецензурное название.
    Цитата:
    как у меня и делается. Нэ?
    А вы не видите отличий от примеров из доки?
    Вы в рабочем классе определяете объект класса потока, а затем передаете объект этого рабочего класса (а вслед за ним тянется и объект потока) в поток. Тут даже предположить какие могут быть последствия сего тяжело, да в принципе и не нужно ибо видно что быдлокод и будет быдлокодом даже если будет компилится, хотя бы из-за самой структуры кода.
    Запись от Avazart размещена 15.01.2016 в 17:28 Avazart на форуме
    Обновил(-а) Avazart 15.01.2016 в 18:32
  8. Старый комментарий
    Аватар для icpu
    Цитата:
    Пока претендует только на нецензурное название.
    будет оно. Без шуток.
    Цитата:
    объект класса потока, а затем передаете объект этого рабочего класса (а вслед за ним тянется и объект потока) в поток
    Эммм, что? Где? Вы думаете, я просто так указатели на QThread использовал, для фана? UPD: Да, была опечатка. Исправил.
    Цитата:
    быдлокод и будет быдлокодом
    Я всё ещё не встретил кода, который бы делал то же самое и компилировался. Быдлокод? Допустим. Напишите лучше, ткните меня в грязь лицом.
    Запись от icpu размещена 20.01.2016 в 07:23 icpu вне форума
    Обновил(-а) icpu 20.01.2016 в 07:40
  9. Старый комментарий
    Аватар для Avazart
    Цитата:
    Эммм, что? Где? Вы думаете, я просто так указатели на QThread использовал, для фана? UPD: Да, была опечатка. Исправил.
    C++ (Qt)
    1
    
    th = new QThread(this);
    Нифиговая опечатка.

    Цитата:
    Напишите лучше, ткните меня в грязь лицом.
    А зачем оно мне? Мне за это никто не заплатит.
    Да и кому действительно надо смотрит в доку и читает книги как советуют.
    Запись от Avazart размещена 20.01.2016 в 12:37 Avazart на форуме
    Обновил(-а) Avazart 20.01.2016 в 12:38
  10. Старый комментарий
    Аватар для icpu
    Цитата:
    Сообщение от Avazart Просмотреть комментарий
    А зачем оно мне? Мне за это никто не заплатит.
    Да и кому действительно надо смотрит в доку и читает книги как советуют.
    Я всё ещё не увидел реализации лучше. Не увидел указаний на ошибки, неточности или что-нибудь в этом духе.

    Вы привязались к созданию потоков, я признал опечатку, исправил. /* Если бы я намеренно делал поток потомком объекта, я бы не хранил явно указатель на него, а фильтровал бы детей. Благо, поток нужен раз в сто лет в обед. */ Тем не менее, указанных вами последствий не наступает, потому как вначале создаётся поток, указатель на который передаётся в объект, а потом контекст объекта передаётся в контекст потока, т.е. поток остаётся в старом контексте. Потоки не блокируются, утечек не происходит, неопределённого поведения так же нет. Даже если не утилизировать созданый на куче поток, он и его данные не утекут с завершением приложения.

    В чём конкретно вы видите ошибку? В том, что я не отнаследовался от класса потока? В том, что выложил черновую заготовку, которую ещё нужно дорабатывать? В том, что я явно вызываю invoke?
    Запись от icpu размещена 20.01.2016 в 12:50 icpu вне форума
  11. Старый комментарий
    Аватар для Avazart
    Цитата:
    Не увидел указаний на ошибки, неточности или что-нибудь в этом духе.
    А ну... о чем спорим если вы даже не видите явных указаний.
    Хотите более явных указаний? Пожалуйста - выкиньте свой код на помойку, и завязывайте с программированием.

    Цитата:
    Вы привязались к созданию потоков, я признал опечатку, исправил
    Опечатка? Я объяснил почему выходят такие опечатки.
    нефиг пихать все в одну кучу, и отводить одному классу кучу задач.
    Есть класс "рабочий" который ответственен за всю работу и передается в класс поток, и есть класс "контролер"
    который создает оба объекты этих классов управляет,связывает итп.
    И все прозрачно, а не так как у вас.

    Но я уже не напоминаю что можно было обойтись использованием QtConcurent что выглядело бы куда проще.

    Цитата:
    Даже если не утилизировать созданый на куче поток, он и его данные не утекут с завершением приложения.
    Да при чем тут утечки, владение/принадлежность прямым образом влияет на тип связывания слотов и как следствие синхронизацию между потоками.
    Запись от Avazart размещена 20.01.2016 в 13:18 Avazart на форуме
    Обновил(-а) Avazart 20.01.2016 в 13:28
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2021, vBulletin Solutions, Inc.