COW and thread safety in Qt
От: Skorodum Россия  
Дата: 10.10.23 09:39
Оценка: 12 (2)
Тут коллега задает вопросы
Автор: andyp
Дата: 09.10.23
по потоко-безапасности и копировании-при-записи (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 сигнала и вызов слота происходят в разных потоках?

Тут надо отделить копирование от потока выполнения кода (слота).

Есть ли гарантия, что это делается единообразно во всех релизах Qt начиная с N.K?

Этот механизм работает с Qt 4, т.е. 20 лет.

Я постарался сделать минимальный пример который демострирует поведение Qt классов с COW при использовании в разных потоках.


  CMakeLists.txt
cmake_minimum_required(VERSION 3.14)

project(cow LANGUAGES CXX)

set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core)

add_executable(cow
    main.cpp
)
target_link_libraries(cow Qt${QT_VERSION_MAJOR}::Core)

  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();
    }
};

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"


Ссылки по теме:
implicit sharing
Threads and Implicitly Shared Classes
Implicit sharing iterator problem
Отредактировано 10.10.2023 11:02 Skorodum . Предыдущая версия . Еще …
Отредактировано 10.10.2023 9:39 Skorodum . Предыдущая версия .
thread qt cow
Re: COW and thread safety in Qt
От: SaZ  
Дата: 10.10.23 10:48
Оценка:
Здравствуйте, Skorodum, Вы писали:

S>Тут коллега задает вопросы
Автор: andyp
Дата: 09.10.23
по потоко-безапасности и копировании-при-записи (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 это скорее про удобство и если нужно так уж сильно заморачиваться, то стоит заюзать другой контейнер.
Re[2]: COW and thread safety in Qt
От: Skorodum Россия  
Дата: 10.10.23 11:11
Оценка: +1
Здравствуйте, SaZ, Вы писали:

SaZ>Как-то сложно, я так глубоко не заморачивался.

Да самому захотелось сформулировать нормально
Там минимальный пример, где изменение одной строчки включает/выключает COW и показывает вызовы всех конструкторов-деструкторов. И попутно показывает как легко в Qt писать асинхронный код.

SaZ>А по поводу сигналов слотов — это отдельный вопрос. Если у нас не прямой вызов (DirectConnection), то всегда делается копия и помещается в эвент луп.

+1 Я большую часть своей карьеры работал с Qt и явной синхронизацией данных с помощью мьютексов и симафоров для многопоточного кода заниматься не приходилось. Я обновоил исходный пост с информацией про соедиения.

SaZ>Собственно недостаток COW в том, что на каждое обращение к контейнеру получается сброс кэша и на очень многоядерных системах при интенсивном обмене может получиться просадка по производительности. Но COW это скорее про удобство и если нужно так уж сильно заморачиваться, то стоит заюзать другой контейнер.

Поэтому я и написал про хрестоматийный случай. Если нужна какая-то особенная производительность, то надо плясать от структур данных дружественных к многоядерности и кэшу.
Re: COW and thread safety in Qt
От: andyp  
Дата: 10.10.23 13:13
Оценка: +1
Здравствуйте, Skorodum, Вы писали:

Спасибо за помощь. Версия для тех, у кого 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 thread
    const 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 running
    return qApp->exec();
}

//#include "main.moc"


Как вопросы появятся, спрошу. Пока покрутить в руках это все надо.
Re[3]: COW and thread safety in Qt
От: SaZ  
Дата: 10.10.23 13:31
Оценка:
Здравствуйте, Skorodum, Вы писали:

S>...


S>Поэтому я и написал про хрестоматийный случай. Если нужна какая-то особенная производительность, то надо плясать от структур данных дружественных к многоядерности и кэшу.


Хорошо. Но вместо вектора чего-либо сделайте свой класс который реализует QSharedData — имхо будет намного нагляднее.
Re[2]: COW and thread safety in Qt
От: Skorodum Россия  
Дата: 10.10.23 14:10
Оценка: -1
Здравствуйте, 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 thread
    const 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 running
    return qApp->exec();
}


Все остальное так же.
qt qthread
Re[3]: COW and thread safety in Qt
От: Skorodum Россия  
Дата: 11.10.23 08:13
Оценка:
Здравствуйте, Skorodum, Вы писали:

Zhendos, чем обусловлен минус?
Re: COW and thread safety in Qt
От: andyp  
Дата: 11.10.23 12:31
Оценка:
Здравствуйте, Skorodum, Вы писали:

S>

S>Делается ли deep или shallow copy и в каком потоке это делается, если emit сигнала и вызов слота происходят в разных потоках?


Проверка на моей версии qt показала:

1.После emit сигнала в текущей нитке аргумент сигнала не меняется
2.В слот в другой нитке приезжает !глубокая! копия аргумента сигнала

От debug-release не зависит. Также, не зависит от того, передаются ли аргументы в сигнал-слот по константным ссылкам или значениям.


Прошу прощения, ошибочка вышла!

]1.После emit сигнала в текущей нитке аргумент сигнала не меняется
2.В слот в другой нитке приезжает !shallow! копия аргумента сигнала. Т.е. о данных надо заботиться самому!

От debug-release не зависит. Также, не зависит от того, передаются ли аргументы в сигнал-слот по константным ссылкам или значениям.

Т.е. как и писал в том треде — опасная это фигня.
Отредактировано 11.10.2023 15:25 andyp . Предыдущая версия . Еще …
Отредактировано 11.10.2023 15:17 andyp . Предыдущая версия .
Re[2]: COW and thread safety in Qt
От: SaZ  
Дата: 11.10.23 14:24
Оценка:
Здравствуйте, 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
Re[2]: COW and thread safety in Qt
От: Skorodum Россия  
Дата: 11.10.23 14:51
Оценка:
Здравствуйте, andyp, Вы писали:

A>Проверка на моей версии qt показала:

A>1.После emit сигнала в текущей нитке аргумент сигнала не меняется
A>2.В слот в другой нитке приезжает !глубокая! копия аргумента сигнала
A>От debug-release не зависит. Также, не зависит от того, передаются ли аргументы в сигнал-слот по константным ссылкам или значениям.

Это на коде адаптированном под версия <5.10 который вы привели выше?
Можете показать отладачную печать?
Re[3]: COW and thread safety in Qt
От: andyp  
Дата: 11.10.23 15:23
Оценка:
Здравствуйте, Skorodum, Вы писали:

S>Это на коде адаптированном под версия <5.10 который вы привели выше?

S>Можете показать отладачную печать?

Код, которым тестировалось (константная ссылка). Для теста по значению поменять определения сигнала-слота в Bar.

  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(const QVector<Foo>&) const;

public slots:
    void dataByValue(const QVector<Foo>& foo)
    {
        qDebug() << QThread::currentThread() << Q_FUNC_INFO << foo.size();
        // 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
        foos = foo;
        qDebug() << QThread::currentThread() << "consumer data" << QString::number( uint64_t(foos.constData()), 16 );
    }
};

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
        qDebug() << QThread::currentThread() << "producer data before signal" << QString::number( uint64_t(anotherBar.foos.constData()), 16 );
        QObject::connect(&anotherBar, &Bar::dataByValueSignal, &bar, &Bar::dataByValue);
        anotherBar.emitSignals();
        qDebug() << QThread::currentThread() << "producer data after signal" << QString::number( uint64_t(anotherBar.foos.constData()), 16 );
        // 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 thread
    const 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 running
    return qApp->exec();
}

#include "main.moc"


Выхлоп release, по значению

Foo::Foo() 0x7f7633163c98 QThread(0x563e12505fe0)
Foo::Foo(Foo&&) 0x7f762c005c58 QThread(0x563e12505fe0)
Foo::~Foo() 0x7f7633163c98 QThread(0x563e12505fe0)
QThread(0x563e12505fe0) producer data before signal "7f762c005c58"
QThread(0x563e12505fe0) producer data after signal "7f762c005c58"
QThread(0x563e12505e20) void Bar::dataByValue(QVector<Foo>) 1
QThread(0x563e12505e20) consumer data "7f762c005c58"
Foo::Foo(const Foo&) 0x7f762c0054d8 QThread(0x563e12505fe0)
Foo::~Foo() 0x7f762c0054d8 QThread(0x563e12505fe0)
QThread(0x563e12505fe0) 0

Выхлоп release, по константной ссылке
Foo::Foo() 0x7f4b1df57c98 QThread(0x5610c684dfe0)
Foo::Foo(Foo&&) 0x7f4b18005c58 QThread(0x5610c684dfe0)
Foo::~Foo() 0x7f4b1df57c98 QThread(0x5610c684dfe0)
QThread(0x5610c684dfe0) producer data before signal "7f4b18005c58"
QThread(0x5610c684dfe0) producer data after signal "7f4b18005c58"
QThread(0x5610c684de20) void Bar::dataByValue(const QVector<Foo>&) 1
QThread(0x5610c684de20) consumer data "7f4b18005c58"
Foo::Foo(const Foo&) 0x7f4b180054d8 QThread(0x5610c684dfe0)
Foo::~Foo() 0x7f4b180054d8 QThread(0x5610c684dfe0)
QThread(0x5610c684dfe0) 0
Re[4]: COW and thread safety in Qt
От: Skorodum Россия  
Дата: 12.10.23 07:50
Оценка:
Здравствуйте, 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
Re[3]: COW and thread safety in Qt
От: Skorodum Россия  
Дата: 12.10.23 07:55
Оценка:
Здравствуйте, SaZ, Вы писали:

A>>Проверка на моей версии qt показала:


A>>1.После emit сигнала в текущей нитке аргумент сигнала не меняется

A>>2.В слот в другой нитке приезжает !глубокая! копия аргумента сигнала

SaZ>Не обязательно было проверять. Это не зависит от потоков, это зависит от типа соединения сигнала слота. Достаточно почитать документацию или посмотреть код =)

SaZ>https://doc.qt.io/qt-6/qt.html#ConnectionType-enum
Так там не было глубокой копии при вызове сигнала, копия была при перемещении локальной переменной в вектор.
В целом implicit sharing ортогонален потокам и работает как и должен между потоками.
Re[5]: COW and thread safety in Qt
От: andyp  
Дата: 12.10.23 08:48
Оценка:
Здравствуйте, Skorodum, Вы писали:

S>Все правильно: у нас все тот же объект, никакой глубокой копии не произошло.


Как же правильно? Получили в двух нитках один и тот же буфер внутри двух разных qtvector в продьюсере и консьюмере. Если общее состояние (счётчик ссылок) и глубокое копирование не защищены каким-то примитивом синхронизации, то будут гонки, если несколько ниток попытаются использовать неконстантные операции со своим контейнером. Если синхронизация есть, то будут тормоза не только той нитки, которой понадобилась копия, но и остальных. О чем и писал ещё в старой ветке обсуждения.

Если Foo только формально константен (внутри есть mutable члены), то будут гонки даже с синхронизацией внутри контейнера — он просто не будет знать, что нужно сериализовать доступ к Foo.

Опять же, как там и писал — опасно все это и чревато тормозами в совсем уж нежданных местах. Весь этот шум про корову идёт с конца 90х имхо. Уже тогда некоторым стало понятно, что втыкать синхронизацию в методы контейнера — не очень хорошая идея. Не та гранулярность получается. Подробности например здесь:

http://www.gotw.ca/publications/optimizations.htm

S>Посмотрите, как измениться вывод, если раскомментировать это

S>
S>//foo.append(Foo()); // <- comment out to see effect of COW
S>


Понятно, что здесь отцепится, и у консьюмера будет буфер со своим указателем.
Re[6]: COW and thread safety in Qt
От: Skorodum Россия  
Дата: 12.10.23 09:20
Оценка:
Здравствуйте, andyp, Вы писали:

A>Здравствуйте, Skorodum, Вы писали:


S>>Все правильно: у нас все тот же объект, никакой глубокой копии не произошло.


A>Как же правильно?

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

A>Получили в двух нитках один и тот же буфер внутри двух разных qtvector в продьюсере и консьюмере. Если общее состояние (счётчик ссылок) и глубокое копирование не защищены каким-то примитивом синхронизации, то будут гонки, если несколько ниток попытаются использовать неконстантные операции со своим контейнером. Если синхронизация есть, то будут тормоза не только той нитки, которой понадобилась копия, но и остальных. О чем и писал ещё в старой ветке обсуждения.

Все однозначно и документировано:

QSharedData provides thread-safe reference counting.


A>Если Foo только формально константен (внутри есть mutable члены), то будут гонки даже с синхронизацией внутри контейнера — он просто не будет знать, что нужно сериализовать доступ к Foo.

Не, от Foo это вообще не зависит, за это отвечает QSharedData.

A>Опять же, как там и писал — опасно все это и чревато тормозами в совсем уж нежданных местах. Весь этот шум про корову идёт с конца 90х имхо. Уже тогда некоторым стало понятно, что втыкать синхронизацию в методы контейнера — не очень хорошая идея. Не та гранулярность получается. Подробности например здесь:

A>http://www.gotw.ca/publications/optimizations.htm
Так чудес никто и не обещает. QVector это компромисное решение, но оно дает выигрышь в большинстве простых случаев. Qt это же не число-дробилка, а GUI в первую очередь. Самая типичная задача это передать данные из какого-то источника в интерфейс. Для таких задач COW это отличное решение.
Я сам недавно что-то затупил и написал кольцевой буфер для передачи данных вместо тупого использования QVector через сигналы-слоты и удивлялся почему у меня нет вообще никакой разницы в производительности.

A>Понятно, что здесь отцепится, и у консьюмера будет буфер со своим указателем.

Re[7]: COW and thread safety in Qt
От: andyp  
Дата: 12.10.23 11:26
Оценка:
Здравствуйте, Skorodum, Вы писали:


A>>Если Foo только формально константен (внутри есть mutable члены), то будут гонки даже с синхронизацией внутри контейнера — он просто не будет знать, что нужно сериализовать доступ к Foo.

S>Не, от Foo это вообще не зависит, за это отвечает QSharedData.

Как он может за что-то отвечать, если у Foo внутри mutable члены, а работают с элементами контейнера через константный указатель? Контейнер даже не будет знать о том, что внутри одного из его объектов что-то поменялось. Константный доступ тоже может требовать синхронизации, а тут эти две вещи спарены. Говорил же, ошибки дизайна имхо.
Re[8]: COW and thread safety in Qt
От: Skorodum Россия  
Дата: 12.10.23 14:45
Оценка:
Здравствуйте, 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. Код из разряда тупых головоломок на собеседование...
Отредактировано 12.10.2023 14:57 Skorodum . Предыдущая версия . Еще …
Отредактировано 12.10.2023 14:55 Skorodum . Предыдущая версия .
mutable qt qvector cow
Re[9]: COW and thread safety in Qt
От: andyp  
Дата: 12.10.23 15:18
Оценка: +1
Здравствуйте, Skorodum, Вы писали:

S>Здравствуйте, andyp, Вы писали:


A>>Как он может за что-то отвечать, если у Foo внутри mutable члены, а работают с элементами контейнера через константный указатель? Контейнер даже не будет знать о том, что внутри одного из его объектов что-то поменялось.

S>Ок, получилось воспроизвести и для этого не нужны никакие потоки, но(!) только ипользуя функцию at, про которую явно сказано, что она только для чтения и никогда не делает глубокую копию.

Можно просто constData использовать и читать по указателю.

Ну в твоем примере это глупо, а если это, скажем, последний элемент, подтянутый из БД и закэшированный внутри класса? Const у методов означает семантическую константность, а не битовую. Для синхронизации важна битовая.

A>>Константный доступ тоже может требовать синхронизации, а тут эти две вещи спарены. Говорил же, ошибки дизайна имхо.

S>Если преднамеренно абьюзить константность и мутабельность, то много где чего сломать можно

Ну не знаю. Имхо, именно разработчики контейнера слепили требования к синхронизации и константность. Они считают, что константный доступ не должен требовать синхронизации.

S>P.S. Код из разряда тупых головоломок на собеседование...


Для ж чего еще форум? Чтобы нечто потыкать палочкой и получше самому разобраться с помощью коллег. Мой пример головоломки на собеседовании — это поведение метода capacity() у QVector. Сделал нечто, приведшее к тому, что твой конкретный вектор отцепился от общего буфера, и у тебя внезапно capacity изменилась. Можно написать трехстрочник и спрашивать, что в нем не так
Re[10]: COW and thread safety in Qt
От: Skorodum Россия  
Дата: 16.10.23 15:05
Оценка: 4 (1)
Здравствуйте, andyp, Вы писали:

В сухом остатке получается, что:
* COW у Qt работает как документированно
* можно сломать контейнеры использующие COW через mutable
* реализовать "надежный" контейнер с COW для любых данных нельзя, см. предыдущий пункт
* если не нравятся Qt-шные контейнеры с COW, то в большинстве случаев можно использовать любые другие даже Qt-шными сигналами и слотами.
cow qt
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.