по потоко-безапасности и копировании-при-записи (COW or implicit sharing) в Qt.
TLDR: Для хрестоматийного использования Qt (когда в одном потоке есть источник данных (сеть/диск/и т.д.) и какая-то обработка в другом потоке, например отображение) можно обойтись без явной синхронизации данных и пользоваться бонусами COW.
Что ты под копированием данных для объектов с COW имеешь в виду?
Документация отвечат очень хорошо:
A deep copy implies duplicating an object. A shallow copy is a reference copy, i.e. just a pointer to a shared data block. Making a deep copy can be expensive in terms of memory and CPU. Making a shallow copy is very fast, because it only involves setting a pointer and incrementing the reference count.
Object assignment (with operator=()) for implicitly shared objects is implemented using shallow copies.
Делается ли deep или shallow copy и в каком потоке это делается, если emit сигнала и вызов слота происходят в разных потоках?
Тут надо отделить копирование от потока выполнения кода (слота).
* Копирование происходит "in all member functions that change the internal data", т.е. во всех не const методах. В каком потоке это происходит не важно.
* Поток, в котором выполняется слот, зависит от типа соединения, по умолчанию "The slot is executed in the receiver's thread". Подробнее тут.
Есть ли гарантия, что это делается единообразно во всех релизах Qt начиная с N.K?
Этот механизм работает с Qt 4, т.е. 20 лет.
Я постарался сделать минимальный пример который демострирует поведение Qt классов с COW при использовании в разных потоках.
#include <QtCore/QCoreApplication>
#include <QtCore/QDebug>
#include <QtCore/QScopedPointer>
#include <QtCore/QThread>
#include <QtCore/QTimer>
#define PRINT_FUNCTION_INFO qDebug() << Q_FUNC_INFO << this << QThread::currentThread();
struct Foo {
Foo() { PRINT_FUNCTION_INFO }
Foo(const Foo&) { PRINT_FUNCTION_INFO }
Foo(Foo&&) { PRINT_FUNCTION_INFO }
Foo& operator=(const Foo&) { PRINT_FUNCTION_INFO return *this; }
Foo& operator=(Foo&&) { PRINT_FUNCTION_INFO return *this; }
~Foo() { PRINT_FUNCTION_INFO }
};
class Bar : public QObject {
Q_OBJECT
public:
explicit Bar(QObject* parent = nullptr) : QObject(parent) {}
void emitSignals() { emit dataByValueSignal(foos); }
QVector<Foo> foos;
signals:
void dataByValueSignal(QVector<Foo>);
public slots:
void dataByValue(QVector<Foo> foo)
{
// It is ok to pass "big" QVector object by value because of COW (but const reference is still better).
// Also can safely modify copy of 'COW class' transfered with signals and slots in another thread.
// Using non-const methods will make deep copy of QVector content behind the scene.
foo.append(Foo()); // <- comment out to see effect of COW
qDebug() << Q_FUNC_INFO << foo.size();
}
};
int main(int argc, char *argv[])
{
// main thread
const Bar bar;
// this code will run in another thread
auto producer = [&bar] {
Bar anotherBar;
anotherBar.foos.append(Foo());
qRegisterMetaType<QVector<Foo>>("QVector<Foo>");
// connect to object in another thread and pass object with COW by value
// no copies of QVector *content* at this point and thread safe
QObject::connect(&anotherBar, &Bar::dataByValueSignal, &bar, &Bar::dataByValue);
anotherBar.emitSignals();
// can safely modify local copy of Qt class
QThread::sleep(1);
anotherBar.foos.pop_front();
qDebug() << QThread::currentThread() << anotherBar.foos.size();
};
const QScopedPointer<QThread> thread(QThread::create(producer));
const QCoreApplication a(argc, argv);
QObject::connect(thread.data(), &QThread::finished, qApp, &QCoreApplication::quit);
QTimer::singleShot(0, qApp, [&thread]{thread->start();}); // start thread when event loop is running
return qApp->exec();
}
#include "main.moc"
по потоко-безапасности и копировании-при-записи (COW or implicit sharing) в Qt.
S>
S>Что ты под копированием данных для объектов с COW имеешь в виду? Звучит так же мутно, как и qtшная документация Делается ли deep или shallow copy и в каком потоке это делается, если emit сигнала и вызов слота происходят в разных потоках? Есть ли гарантия, что это делается единообразно во всех релизах Qt начиная с N.K? Я не нашел внятного ответа на этот вопрос в свое время и всегда детачил копию контейнера перед тем как ее в сигнал засовывать. Ну это пока еще использовал эти контейнеры вообще.
S>Я постарался сделать минимальный пример который демострирует поведение Qt классов с COW при использовании в разных потоках. S>TLDR: Для хрестоматийного использования Qt (когда в одном потоке есть источник данных (сеть/диск/и т.д.) и какая-то обработка в другом потоке, например отображение) можно обойтись без явной синхронизации данных и пользоваться бонусами COW. И все это рабоет с 4-й версии без изменений (почти 20 лет).
S> S>...
Как-то сложно, я так глубоко не заморачивался. COW он примерно как шаред поинтер. Под капотом там QSharedData. То есть плодить экземпляры можно потокобезопасно без лишних заморочек, счётчик на атомиках. Единственное чего не помню, за счёт чего обеспечивается глубокое копирование. Наверное там на спинлоках, иначе было бы медленно.
А по поводу сигналов слотов — это отдельный вопрос. Если у нас не прямой вызов (DirectConnection), то всегда делается копия и помещается в эвент луп.
Собственно недостаток COW в том, что на каждое обращение к контейнеру получается сброс кэша и на очень многоядерных системах при интенсивном обмене может получиться просадка по производительности. Но COW это скорее про удобство и если нужно так уж сильно заморачиваться, то стоит заюзать другой контейнер.
Здравствуйте, SaZ, Вы писали:
SaZ>Как-то сложно, я так глубоко не заморачивался.
Да самому захотелось сформулировать нормально
Там минимальный пример, где изменение одной строчки включает/выключает COW и показывает вызовы всех конструкторов-деструкторов. И попутно показывает как легко в Qt писать асинхронный код.
SaZ>А по поводу сигналов слотов — это отдельный вопрос. Если у нас не прямой вызов (DirectConnection), то всегда делается копия и помещается в эвент луп.
+1 Я большую часть своей карьеры работал с Qt и явной синхронизацией данных с помощью мьютексов и симафоров для многопоточного кода заниматься не приходилось. Я обновоил исходный пост с информацией про соедиения.
SaZ>Собственно недостаток COW в том, что на каждое обращение к контейнеру получается сброс кэша и на очень многоядерных системах при интенсивном обмене может получиться просадка по производительности. Но COW это скорее про удобство и если нужно так уж сильно заморачиваться, то стоит заюзать другой контейнер.
Поэтому я и написал про хрестоматийный случай. Если нужна какая-то особенная производительность, то надо плясать от структур данных дружественных к многоядерности и кэшу.
Спасибо за помощь. Версия для тех, у кого QT < 5.10
main.cpp
#include <QtCore/QCoreApplication>
#include <QtCore/QDebug>
#include <QtCore/QScopedPointer>
#include <QtCore/QThread>
#include <QtCore/QTimer>
#define PRINT_FUNCTION_INFO qDebug() << Q_FUNC_INFO << this << QThread::currentThread();
struct Foo {
Foo() { PRINT_FUNCTION_INFO }
Foo(const Foo&) { PRINT_FUNCTION_INFO }
Foo(Foo&&) { PRINT_FUNCTION_INFO }
Foo& operator=(const Foo&) { PRINT_FUNCTION_INFO return *this; }
Foo& operator=(Foo&&) { PRINT_FUNCTION_INFO return *this; }
~Foo() { PRINT_FUNCTION_INFO }
};
class Bar : public QObject {
Q_OBJECT
public:
explicit Bar(QObject* parent = nullptr) : QObject(parent) {}
void emitSignals() { emit dataByValueSignal(foos); }
QVector<Foo> foos;
signals:
void dataByValueSignal(QVector<Foo>);
public slots:
void dataByValue(QVector<Foo> foo)
{
// It is ok to pass "big" QVector object by value because of COW (but const reference is still better).
// Also can safely modify copy of 'COW class' transfered with signals and slots in another thread.
// Using non-const methods will make deep copy of QVector content behind the scene.
foo.append(Foo()); // <- comment out to see effect of COW
qDebug() << Q_FUNC_INFO << foo.size();
}
};
class Producer : public QObject
{
Q_OBJECT
const Bar& bar;
public:
Producer(const Bar& b): bar(b){}
public slots:
void do_produce() {
Bar anotherBar;
anotherBar.foos.append(Foo());
// connect to object in another thread and pass object with COW by value
// no copies of QVector *content* at this point and thread safe
QObject::connect(&anotherBar, &Bar::dataByValueSignal, &bar, &Bar::dataByValue);
anotherBar.emitSignals();
// can safely modify local copy of Qt class
QThread::sleep(1);
anotherBar.foos.pop_front();
qDebug() << QThread::currentThread() << anotherBar.foos.size();
}
};
int main(int argc, char *argv[])
{
// main threadconst Bar bar;
qRegisterMetaType<QVector<Foo>>("QVector<Foo>");
const QScopedPointer<QThread> thread(new QThread);
Producer prod(bar);
prod.moveToThread(thread.data());
const QCoreApplication a(argc, argv);
QObject::connect(thread.data(), &QThread::started, &prod, &Producer::do_produce);
QObject::connect(thread.data(), &QThread::finished, qApp, &QCoreApplication::quit);
QTimer::singleShot(0, qApp, [&thread]{thread->start();}); // start thread when event loop is runningreturn qApp->exec();
}
//#include "main.moc"
Как вопросы появятся, спрошу. Пока покрутить в руках это все надо.
Здравствуйте, Skorodum, Вы писали:
S>...
S>Поэтому я и написал про хрестоматийный случай. Если нужна какая-то особенная производительность, то надо плясать от структур данных дружественных к многоядерности и кэшу.
Хорошо. Но вместо вектора чего-либо сделайте свой класс который реализует QSharedData — имхо будет намного нагляднее.
Здравствуйте, andyp, Вы писали:
A>Здравствуйте, Skorodum, Вы писали:
A>Спасибо за помощь. Версия для тех, у кого QT < 5.10
Дополню альтернативным методом использованию QThread: через наследование и переопределение метода run.
class Producer : public QThread
{
Q_OBJECT
const Bar& bar;
public:
Producer(const Bar& b, QObject *parent = nullptr): QThread(parent), bar(b){}
public slots:
void run() {
Bar anotherBar;
anotherBar.foos.append(Foo());
// connect to object in another thread and pass object with COW by value
// no copies of QVector *content* at this point and thread safe
QObject::connect(&anotherBar, &Bar::dataByValueSignal, &bar, &Bar::dataByValue);
anotherBar.emitSignals();
// can safely modify local copy of Qt class
QThread::sleep(1);
anotherBar.foos.pop_front();
qDebug() << QThread::currentThread() << anotherBar.foos.size();
}
};
int main(int argc, char *argv[])
{
// main threadconst Bar bar;
qRegisterMetaType<QVector<Foo>>("QVector<Foo>");
QCoreApplication a(argc, argv);
Producer *producer(new Producer(bar, &a)); // NOTE: application takes ownership
QObject::connect(producer, &QThread::finished, qApp, &QCoreApplication::quit);
QTimer::singleShot(0, qApp, [&producer]{producer->start();}); // start thread when event loop is runningreturn qApp->exec();
}
S>Делается ли deep или shallow copy и в каком потоке это делается, если emit сигнала и вызов слота происходят в разных потоках?
Проверка на моей версии qt показала:
1.После emit сигнала в текущей нитке аргумент сигнала не меняется
2.В слот в другой нитке приезжает !глубокая! копия аргумента сигнала
От debug-release не зависит. Также, не зависит от того, передаются ли аргументы в сигнал-слот по константным ссылкам или значениям.
Прошу прощения, ошибочка вышла!
]1.После emit сигнала в текущей нитке аргумент сигнала не меняется
2.В слот в другой нитке приезжает !shallow! копия аргумента сигнала. Т.е. о данных надо заботиться самому!
От debug-release не зависит. Также, не зависит от того, передаются ли аргументы в сигнал-слот по константным ссылкам или значениям.
Здравствуйте, andyp, Вы писали:
A>Здравствуйте, Skorodum, Вы писали:
S>>
S>>Делается ли deep или shallow copy и в каком потоке это делается, если emit сигнала и вызов слота происходят в разных потоках?
A>Проверка на моей версии qt показала:
A>1.После emit сигнала в текущей нитке аргумент сигнала не меняется A>2.В слот в другой нитке приезжает !глубокая! копия аргумента сигнала
Не обязательно было проверять. Это не зависит от потоков, это зависит от типа соединения сигнала слота. Достаточно почитать документацию или посмотреть код =) https://doc.qt.io/qt-6/qt.html#ConnectionType-enum
Здравствуйте, andyp, Вы писали:
A>Проверка на моей версии qt показала: A>1.После emit сигнала в текущей нитке аргумент сигнала не меняется A>2.В слот в другой нитке приезжает !глубокая! копия аргумента сигнала A>От debug-release не зависит. Также, не зависит от того, передаются ли аргументы в сигнал-слот по константным ссылкам или значениям.
Это на коде адаптированном под версия <5.10 который вы привели выше?
Можете показать отладачную печать?
Здравствуйте, andyp, Вы писали:
A>Foo::Foo() 0x7f7633163c98 QThread(0x563e12505fe0) A>Foo::Foo(Foo&&) 0x7f762c005c58 QThread(0x563e12505fe0) A>Foo::~Foo() 0x7f7633163c98 QThread(0x563e12505fe0)
Этот вывод соответсвуют этому коду:
anotherBar.foos.append(Foo());
A>QThread(0x563e12505fe0) producer data before signal "7f762c005c58" A>QThread(0x563e12505fe0) producer data after signal "7f762c005c58" A>QThread(0x563e12505e20) void Bar::dataByValue(QVector<Foo>) 1 A>QThread(0x563e12505e20) consumer data "7f762c005c58"
Все правильно: у нас все тот же объект, никакой глубокой копии не произошло.
A>Foo::Foo(const Foo&) 0x7f762c0054d8 QThread(0x563e12505fe0) A>Foo::~Foo() 0x7f762c0054d8 QThread(0x563e12505fe0) A>QThread(0x563e12505fe0) 0
Посмотрите, как измениться вывод, если раскомментировать это
//foo.append(Foo()); // <- comment out to see effect of COW
Здравствуйте, SaZ, Вы писали:
A>>Проверка на моей версии qt показала:
A>>1.После emit сигнала в текущей нитке аргумент сигнала не меняется A>>2.В слот в другой нитке приезжает !глубокая! копия аргумента сигнала
SaZ>Не обязательно было проверять. Это не зависит от потоков, это зависит от типа соединения сигнала слота. Достаточно почитать документацию или посмотреть код =) SaZ>https://doc.qt.io/qt-6/qt.html#ConnectionType-enum
Так там не было глубокой копии при вызове сигнала, копия была при перемещении локальной переменной в вектор.
В целом implicit sharing ортогонален потокам и работает как и должен между потоками.
Здравствуйте, Skorodum, Вы писали:
S>Все правильно: у нас все тот же объект, никакой глубокой копии не произошло.
Как же правильно? Получили в двух нитках один и тот же буфер внутри двух разных qtvector в продьюсере и консьюмере. Если общее состояние (счётчик ссылок) и глубокое копирование не защищены каким-то примитивом синхронизации, то будут гонки, если несколько ниток попытаются использовать неконстантные операции со своим контейнером. Если синхронизация есть, то будут тормоза не только той нитки, которой понадобилась копия, но и остальных. О чем и писал ещё в старой ветке обсуждения.
Если Foo только формально константен (внутри есть mutable члены), то будут гонки даже с синхронизацией внутри контейнера — он просто не будет знать, что нужно сериализовать доступ к Foo.
Опять же, как там и писал — опасно все это и чревато тормозами в совсем уж нежданных местах. Весь этот шум про корову идёт с конца 90х имхо. Уже тогда некоторым стало понятно, что втыкать синхронизацию в методы контейнера — не очень хорошая идея. Не та гранулярность получается. Подробности например здесь:
Здравствуйте, andyp, Вы писали:
A>Здравствуйте, Skorodum, Вы писали:
S>>Все правильно: у нас все тот же объект, никакой глубокой копии не произошло.
A>Как же правильно?
Правильно в том смысле, что происходит так, как и должно происходить согласно документации: глубокой копии не происходит, пока кто-то не пытается менять объект.
A>Получили в двух нитках один и тот же буфер внутри двух разных qtvector в продьюсере и консьюмере. Если общее состояние (счётчик ссылок) и глубокое копирование не защищены каким-то примитивом синхронизации, то будут гонки, если несколько ниток попытаются использовать неконстантные операции со своим контейнером. Если синхронизация есть, то будут тормоза не только той нитки, которой понадобилась копия, но и остальных. О чем и писал ещё в старой ветке обсуждения.
Все однозначно и документировано:
A>Если Foo только формально константен (внутри есть mutable члены), то будут гонки даже с синхронизацией внутри контейнера — он просто не будет знать, что нужно сериализовать доступ к Foo.
Не, от Foo это вообще не зависит, за это отвечает QSharedData.
A>Опять же, как там и писал — опасно все это и чревато тормозами в совсем уж нежданных местах. Весь этот шум про корову идёт с конца 90х имхо. Уже тогда некоторым стало понятно, что втыкать синхронизацию в методы контейнера — не очень хорошая идея. Не та гранулярность получается. Подробности например здесь: A>http://www.gotw.ca/publications/optimizations.htm
Так чудес никто и не обещает. QVector это компромисное решение, но оно дает выигрышь в большинстве простых случаев. Qt это же не число-дробилка, а GUI в первую очередь. Самая типичная задача это передать данные из какого-то источника в интерфейс. Для таких задач COW это отличное решение.
Я сам недавно что-то затупил и написал кольцевой буфер для передачи данных вместо тупого использования QVector через сигналы-слоты и удивлялся почему у меня нет вообще никакой разницы в производительности.
A>Понятно, что здесь отцепится, и у консьюмера будет буфер со своим указателем.
A>>Если Foo только формально константен (внутри есть mutable члены), то будут гонки даже с синхронизацией внутри контейнера — он просто не будет знать, что нужно сериализовать доступ к Foo. S>Не, от Foo это вообще не зависит, за это отвечает QSharedData.
Как он может за что-то отвечать, если у Foo внутри mutable члены, а работают с элементами контейнера через константный указатель? Контейнер даже не будет знать о том, что внутри одного из его объектов что-то поменялось. Константный доступ тоже может требовать синхронизации, а тут эти две вещи спарены. Говорил же, ошибки дизайна имхо.
Здравствуйте, andyp, Вы писали: A>Как он может за что-то отвечать, если у Foo внутри mutable члены, а работают с элементами контейнера через константный указатель? Контейнер даже не будет знать о том, что внутри одного из его объектов что-то поменялось.
Ок, получилось воспроизвести и для этого не нужны никакие потоки, но(!) только ипользуя функцию at, про которую явно сказано, что она только для чтения и никогда не делает глубокую копию.
код
#include <QtCore/QCoreApplication>
#include <QtCore/QDebug>
int main(int, char *[])
{
struct Foo
{
mutable int data = -1; // <- we are very 'smart'
};
const auto vector = QVector<Foo>{Foo()};
[vector]{ // capture by value, this makes copy of QVector, but content is implicitly shared
// function "at" must be used for read-only access,
// since it returns const reference and never makes deep copy.
// It is still possible to modify data marked as mutable and make
// changes which affects other owners of implicitly shared data.
vector.at(0).data = 42;
// foo[0].data = 42; // <- right way to do
}();
// Ops: our local constant data is modified in another place because COW is broken with mutable!
qDebug() << vector.at(0).data;
return 0;
}
A>Константный доступ тоже может требовать синхронизации, а тут эти две вещи спарены. Говорил же, ошибки дизайна имхо.
Если преднамеренно абьюзить константность и мутабельность, то много где чего сломать можно
P.S. Код из разряда тупых головоломок на собеседование...
Здравствуйте, Skorodum, Вы писали:
S>Здравствуйте, andyp, Вы писали:
A>>Как он может за что-то отвечать, если у Foo внутри mutable члены, а работают с элементами контейнера через константный указатель? Контейнер даже не будет знать о том, что внутри одного из его объектов что-то поменялось. S>Ок, получилось воспроизвести и для этого не нужны никакие потоки, но(!) только ипользуя функцию at, про которую явно сказано, что она только для чтения и никогда не делает глубокую копию.
Можно просто constData использовать и читать по указателю.
Ну в твоем примере это глупо, а если это, скажем, последний элемент, подтянутый из БД и закэшированный внутри класса? Const у методов означает семантическую константность, а не битовую. Для синхронизации важна битовая.
A>>Константный доступ тоже может требовать синхронизации, а тут эти две вещи спарены. Говорил же, ошибки дизайна имхо. S>Если преднамеренно абьюзить константность и мутабельность, то много где чего сломать можно
Ну не знаю. Имхо, именно разработчики контейнера слепили требования к синхронизации и константность. Они считают, что константный доступ не должен требовать синхронизации.
S>P.S. Код из разряда тупых головоломок на собеседование...
Для ж чего еще форум? Чтобы нечто потыкать палочкой и получше самому разобраться с помощью коллег. Мой пример головоломки на собеседовании — это поведение метода capacity() у QVector. Сделал нечто, приведшее к тому, что твой конкретный вектор отцепился от общего буфера, и у тебя внезапно capacity изменилась. Можно написать трехстрочник и спрашивать, что в нем не так
В сухом остатке получается, что:
* COW у Qt работает как документированно
* можно сломать контейнеры использующие COW через mutable
* реализовать "надежный" контейнер с COW для любых данных нельзя, см. предыдущий пункт
* если не нравятся Qt-шные контейнеры с COW, то в большинстве случаев можно использовать любые другие даже Qt-шными сигналами и слотами.