LINUX.ORG.RU

Нужна удобная обёртка над асинхронным кодом в Qt

 ,


1

5

Собственно задача стара как мир: есть дочерний поток для тяжелых задач, с которым нужно общаться из основного потока.

Моя, дополнительная задача состоит в том, чтобы скрыть всю головную боль от общения между потоками в C++ в удобную обёртку.

Сама задача, в целом, решена, но интересуют мелкие нюансы.

Полный, рабочий пример кода залил на github. Основной класс: worker.cpp worker.h

В комментариях подробно описаны мои проблемы с текущей реализацией.

Дублирую основные вопросы, для удобства:

  1. Безопасно ли использовать bool в данном коде, или стоит использовать atomic/mutex(для сложный объектов)?
  2. Есть ли более простой способ вызова методов, без QMetaObject::invokeMethod и макросов?
  3. Можно ли избежать дублирования сигналов?
  4. Есть ли более простые (меньше кода), готовые реализации?

* в данном примере Worker'а не заботят логические ошибки, типа вызова resume, до start. Этим занимается родительский код.

cast eao197

Ответ на: комментарий от eao197

Qt'ная обёртка для pimpl:

#define Q_DECLARE_PRIVATE_D(Dptr, Class) \
    inline Class##Private* d_func() { return reinterpret_cast<Class##Private *>(Dptr); } \
    inline const Class##Private* d_func() const { return reinterpret_cast<const Class##Private *>(Dptr); } \
    friend class Class##Private;

#define Q_D(Class) Class##Private * const d = d_func()
#define Q_Q(Class) Class * const q = q_func()

RazrFalcon ★★★★★ ()
Последнее исправление: RazrFalcon (всего исправлений: 2)
Ответ на: комментарий от RazrFalcon

Теперь понятно откуда внезапно берется локальная переменная d

Я так понимаю, вы в Worker-е используете какую-то Qt-шную магию, за счет которой происходит делегирование вызова на другую нить. Наверное, с этим будет все нормально.

А вот там, где вы bool-евые переменные в одном потоке просто устанавливаете (при входе-выходе в WorkerPrivate::doStuff), а в другом потоке просто читаете (например, вызывая WorkerPrivate::isBusy() из Worker-а), вы можете не видеть изменения WorkerPrivate::m_isBusy во втором потоке. По крайней мере сразу.

Вообще, если не хотите брать готовые библиотеки, то вся эта кухня с отдельной нитью worker-ом и очередью тасков к ней на C++11 пишется за полчаса-час.

eao197 ★★★★★ ()
Ответ на: комментарий от eao197

По крайней мере сразу.

Это может привести к проблемам в текущей реализации?

В данном случае меня волнует передача чего-то сложнее, чем притивы. К примеру QStringList, который нужно передавать в приватный поток. Через сигналы/слоты это делать тупо, так как в теории есть шанс, что я вызову старт до того, как придёт QStringList. Хотя, QEventLoop вроде как гарантирует очередь при Qt::QueuedConnection. Но это не точно.

вся эта кухня с отдельной нитью worker-ом и очередью тасков к ней

Нить у меня работает всё время работы проги. И задания она выполняет одни и те же. Меняются только входные данные.

Готовые реализации task-based parallelism на C++ я видел. Но у них у всех одни и те же проблемы (к примеру у async++):

  1. они не умеют в паузу/отмену, так как не зависят от цикла событий, что для меня критично
  2. естественно не умеют в сигналы-слоты, из-за чего нет возможности сообщать прогресс
RazrFalcon ★★★★★ ()
Ответ на: комментарий от RazrFalcon

Это может привести к проблемам в текущей реализации?

Боюсь, вам нужна консультация кого-то поопытнее в низкоуровневых деталях. Как бы вы не наткнулись на то, что после строки:

m_isBusy = false;
будет изменены данные только в кэше того ядра, на котором работает ваш WorkerPrivate. И когда у вас Worker будет работать на другом ядре, он может видеть старое значение m_isBusy в своем кэше. Соответственно, Worker может висеть в цикле на m_isBusy не подозревая, что значение этого m_isBusy поменялось.

Но я здесь уже не копенганен, сорри.

eao197 ★★★★★ ()
Ответ на: комментарий от eao197

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

Если говорить за современную java, то volatile в принципе будет эквивалентен атомику за вычетом некоторых полезных функций, который несет с собой атомик. Ну, java memory model, все дела

dave ★★★★★ ()
Ответ на: комментарий от dave

В C++ volatile != atomic. Поэтому объявлять булевые флаги volatile для достижения видимости между разными потоками нет смысла.

eao197 ★★★★★ ()
Ответ на: комментарий от eao197

Да тут кто-то на форуме недавно вещал, что современные компиляторы Си++ де вставляют барьеры памяти для volatile. А так, да, неэквивалентен. В старой java (до 1.4, кажется) - тоже, пока не объявили о «java memory model»

dave ★★★★★ ()
Ответ на: комментарий от dave

Да тут кто-то на форуме недавно вещал, что современные компиляторы Си++ де вставляют барьеры памяти для volatile.

В интернетах полно информации (как старой, так и новой) по поводу того, почему C++ный volatile не подходит для использования в многопоточности для передачи информации между нитями.

eao197 ★★★★★ ()
Ответ на: комментарий от RazrFalcon

man C++11 atomics и модель памяти C++ нужно писать/читать в atomic переменные с соответствующим memory order

lberserq ()
Ответ на: комментарий от eao197

Потому что volatile гарантирует только вычитывание переменной каждый раз при обращении из памяти, а на reordering обращений ему покласть

lberserq ()
Ответ на: комментарий от x905

В той теме вообще про другое.

кидай сигналы

Как?

Q_D

Обычный PIMPL.

m_isBusy

Да. Уже брал его.

m_isForceStop

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

см. https://wiki.qt.io/QThreads_general_usage

У меня так же реализовано.

Обновил сорцы. Другой подход к остановке. stop теперь частично блокирующий, ибо иначе от него толку 0. Так как мне всё равно нельзя снова запускать работу, пока предыдущая не завершилась.

RazrFalcon ★★★★★ ()
Ответ на: комментарий от x905

it's a magic stuff

либо в qt 5.9 - это не так, либо магия.

#include <QCoreApplication>
#include <QtConcurrent>
#include <QFutureWatcher>
#include <QFuture>
#include <QThread>
#include <QtDebug>



class Worker : public QObject {
	Q_OBJECT
public:
	Worker(QObject* parent = nullptr) : QObject(parent) {

	}
	~Worker () {}

	void workEba() {
		qDebug() << "start2";
		qDebug() << QThread::currentThreadId();
		QThread::sleep(5);
		emit signalTest("1");
		QThread::sleep(5);
		emit signalTest("2");
		QThread::sleep(5);
		emit signalTest("3");
		qDebug() << "end2";
		qDebug() << QThread::currentThreadId();
	}
signals:
	void signalTest(QString);
};

class Worker2 : public QObject {
	Q_OBJECT
public:
	Worker2(QObject* parent = nullptr) : QObject(parent) {

	}
	~Worker2 () {}
public slots:
	void testSlot(QString t) {
		qDebug() << "start3";
		qDebug() << QThread::currentThreadId();
		qDebug() << "text->" << t;
	}

	void testSlot2() {
		qDebug() << "start4";
		qDebug() << QThread::currentThreadId();
		qApp->exit(0);
	}
};

class Worker3 : public QObject {
	Q_OBJECT
public:
	Worker3(QObject* parent = nullptr) : QObject(parent) {

	}
	~Worker3 () {}

public Q_SLOTS:
	void run() {
		qDebug() << "start1";
		qDebug() << QThread::currentThreadId();
		Worker* w = new Worker();
		Worker2* w2 = new Worker2();
		bool b = QObject::connect(w, SIGNAL(signalTest(QString)), w2, SLOT(testSlot(QString)));
		qDebug() << "b->" << b;

		QFutureWatcher<void>* fw = new QFutureWatcher<void>();
		QObject::connect(fw, SIGNAL(finished()), w2, SLOT(testSlot2()));
		QObject::connect(fw, SIGNAL(canceled()), w2, SLOT(testSlot2()));
		fw->setFuture(QtConcurrent::run(w, &Worker::workEba));
	}
};

#include "main.moc"

int main(int argc, char *argv[])
{
	QCoreApplication a(argc, argv);
	Worker3 w3;
	QTimer timer;
	timer.setSingleShot(true);
	QObject::connect(&timer, SIGNAL(timeout()), &w3, SLOT(run()));
	timer.start(100);

	return a.exec();
}

даёт такой вывод

/mnt/media/projects/build-qt-future-test-Desktop-Debug/qt-future-test
start1
0x7ff18310f0c0
b-> true
start2
0x7ff17d5a1700
start3
0x7ff18310f0c0
text-> "1"
start3
0x7ff18310f0c0
text-> "2"
end2
0x7ff17d5a1700
start3
0x7ff18310f0c0
text-> "3"
start4
0x7ff18310f0c0

dhampire ★★★ ()
Ответ на: комментарий от x905

emit в обе стороны

Я это и делаю, только с меньшим количеством кода и ненужных методов.

тут его нету

С чего бы это?

а еще я увидел eventloop - зачем ?

Что значит зачем? Для работы сигналов/слотов, таймеров и прочего.

RazrFalcon ★★★★★ ()
Ответ на: комментарий от RazrFalcon

Я это и делаю, только с меньшим количеством кода и ненужных методов.

это ручным то вызовом invokeMethod ?

С чего бы это?

а с чего ему тут быть ?
все что ты сделал, объявил его приватным

Для работы сигналов/слотов, таймеров и прочего.

он уже есть, еще один loop не нужен

x905 ★★★★★ ()
Ответ на: it's a magic stuff от dhampire

погляжу, возможно мои знания устарели, не часто использовал QtConcurrent::run

x905 ★★★★★ ()
Ответ на: комментарий от RazrFalcon

В демонстрации наверно. Из произвольного потока можно отправлять сигналы в поток с Qt eventloop для выполнения в его контексте слота связанного с отправленным сигналом.

dhampire ★★★ ()
Ответ на: комментарий от annulen

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

К примеру они используют статический QReadWriteLock тут, и я не пойму зачем и почему. Это же небезопасно, вроде как.

RazrFalcon ★★★★★ ()
Последнее исправление: RazrFalcon (всего исправлений: 2)
Ответ на: комментарий от x905

Не вижу ничего простого в вашем коде.

Использовать QtConcurrent::run и QFutureWatcher вместо moveToThread - это перебор.

Ещё и куча возни с указателями.

Я, пока что, остановился на ThreadWeaver. Он не идеален, но лучше я не нашел.

Разве что собирать его под виндой - ад.

RazrFalcon ★★★★★ ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.