В одной из прошлых статей “О Многопоточности в Qt и как создать поток” рассмотрены различные способы выполнения кода в отдельном потоке при использовании фреймворка Qt. Один из способов: наследование от класса QThread и переопределение метода run() который только ленивый уже не обругал и не заклеймил. Также поступил и я но поразмыслив решил взять свои слова обратно и подробнее рассказать об этом способе и его особенностях.
Содержание статьи 1. Как это работает? 2. Пример использования? 3. Достоинства и недостатки
В прошлой статье упоминалось, что использование метода противоречит принципам SOLID. А именно принципу открытости\закрытости (O – “open\closed principle”). Но принцип на то и принцип, что дает лишь общее абстрактное описание, не опираясь на конкретную задачу, а потому возможные некоторые вариации. Разработчику приходится самому “дозировать” те или иные принципы в своих задачах. Поэтому отбрасывать один из способов создания потока только из-за того, что один из принципов слабо реализуется, как минимум не корректно.
1. Как это работает?
Если вы знакомы с языком Java, то это очень похоже на реализацию интерфейса Runnable. Создайте класс с предком QThread и переопределите метод run(). Этот метод будет запущен в отдельном потоке, а все остальные методы-члены и параметры класса, как и сам экземпляр класса продолжат своё существование в том потоке, где был аллоцирован экземпляр.
class ThreadMethod : public QThread {
private:
void run() override {
// this code is executed in another thread
}
};
Такая реализация позволяет инкапсулировать разделяемые данные (некий буфер) и саму задачу (метод исполняемый в отдельном потоке) в одном классе.
Важно помнить, что в Qt объекты существуют в том потоке, где они созданы, а при взаимодействии с некоторыми классами из других потоков может привести к неопределённому поведению (Undefined behavior).
2. Пример использования?
В одной из программ, разрабатываемых мною, нужен был синхронный доступ к последовательному порту. Доступ к периферии может занимать много времени, поэтому во избежание “заморозки” и подвисания основного потока объекты взаимодействующие с периферией выносятся в другие потоки.
Реализация класса представлена ниже
class DataModel : public QThread {
public:
DataModel(QStringView port, qint32 baud, int timeout_ms = 0)
: m_port(port.toString()),
m_baud(baud),
m_timeout(timeout_ms)
{}
// Останавливаем выполнение потока и дожидаемся окончания
~DataModel() {
stop();
wait();
}
// Метод отправки данных в последовательный порт
void send(const QByteArray& msg) {
QMutexLocker lk(&m_mtx);
m_queue.enqueue(msg);
if(isRunning()) {
m_sem.release();
} else {
start();
}
}
// Метод остановки потока
void stop() {
QMutexLocker lk(&m_mtx);
m_stop = true;
if(isRunning() && m_sem.available() == 0)
m_sem.release();
}
private:
// метод, работающий в отдельном потоке
void run() override {
QScopedPointer<QSerialPort> port(new QSerialPort());
port->setPortName(m_port);
port->setBaudRate(m_baud);
if(!port->open(QIODevice::ReadWrite) {
emit errorOccured(QString("Can't open %1, error code %2").arg(m_port).arg(port.error()));
return;
}
emit connected(true);
do {
m_mtx.lock();
auto txBuffer = m_queue.dequeue();
m_mtx.unlock();
m_port.write(txBuffer);
if(m_port.waitForBytesWritten(m_timeout)) {
if(m_port.waitForReadyRead(m_timeout)) {
auto rxBuffer = m_port.readAll();
emit readyRead(rxBuffer);
} else {
emit errorOccured("Read timeout");
}
} else {
emit errorOccured("Can't write data");
}
if(m_sem.available() > 0) {
m_sem.acquire();
} else {
emit queueIsEmpty();
}
} while(!m_stop);
if(m_port.isOpen())
m_port.close();
emit connected(false);
}
QQueue<QByteArray> m_queue;
QSemaphore m_sem;
QMutex m_mtx;
bool m_stop {false};
QString m_port;
qint32 m_baud;
int m_timeout;
singals:
void readyRead(const QByteArray&);
void connected(bool);
void errorOccured(const QString&);
void queueIsEmpty();
};
Работа с классом осуществляется следующим образом. Создается экземпляр класса, в конструктор которого передается имя последовательного порта, бод рейт и таймаут в мс. Если понадобиться остановить и уничтожить класс, вызывается метод void stop(). Новые данные передаются в качестве аргумента в метод void send(const QByteArray&) и складываются в очередь. Если это первая попытка передачи данных, то запускается поток на выполнение, в противном случае, инкрементируем семафор.
При запуске потока производится подключение к последовательному порту. Если подключение не удалось, то излучаем сигнал с текстом ошибки и выходим из метода void run(). При завершении метода void run() поток будет завершен.
Далее попадаем в цикл, где берем данные из очереди и записываем их в порт и вызываем waitForBytesWritten() – это блокирующий метод, осуществляющий запись данных в порт в течение заданного таймаута (или оставьте аргумент пустым, чтобы метод ждал бесконечно до записи данных). Затем ожидаем чтения данных из порта и копируем их в rxBuffer. Полученные данные выводим в сигнале readyRead(const QByteArray&).
Затем проверяем есть ли необработанные состояния семафора. Если есть – обрабатываем, если нет, выдаем сигнал queueIsEmpty().
3. Достоинства и недостатки
- Прост в реализации, просто наследуйте класс QThread и переопределите метод void run();
- Позволяет инкапсулировать разделяемые данные, нет необходимости создания отдельного класса для реализации потокобезопасной структуры хранения данных.
- Отсутствует обработчик событий, поэтому использование сигналов для объектов, созданных внутри метода run() не возможно.
Благодарю за внимание! В следующей статье рассмотрим применение асинхронности в Qt и C++ как более высокоуровнего API для многопоточных разработок