Информация об изменениях

Сообщение COW and thread safety in Qt от 10.10.2023 9:39

Изменено 10.10.2023 11:02 Skorodum

COW and thread safety in Qt
Тут коллега задает вопросы
Автор: andyp
Дата: 09.10.23
по потоко-безапасности и копировании-при-записи (COW or implicit sharing) в Qt.

Что ты под копированием данных для объектов с COW имеешь в виду? Звучит так же мутно, как и qtшная документация Делается ли deep или shallow copy и в каком потоке это делается, если emit сигнала и вызов слота происходят в разных потоках? Есть ли гарантия, что это делается единообразно во всех релизах Qt начиная с N.K? Я не нашел внятного ответа на этот вопрос в свое время и всегда детачил копию контейнера перед тем как ее в сигнал засовывать. Ну это пока еще использовал эти контейнеры вообще.


Я постарался сделать минимальный пример который демострирует поведение Qt классов с COW при использовании в разных потоках.
TLDR: Для хрестоматийного использования Qt (когда в одном потоке есть источник данных (сеть/диск/и т.д.) и какая-то обработка в другом потоке, например отображение) можно обойтись без явной синхронизации данных и пользоваться бонусами COW. И все это рабоет с 4-й версии без изменений (почти 20 лет).

  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
thread qt cow
COW and thread safety in Qt
Тут коллега задает вопросы
Автор: 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 сигнала и вызов слота происходят в разных потоках?

Тут надо отделить копирование от потока выполнения кода (слота).
    * Копирование происходит "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 при использовании в разных потоках.


  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
thread qt cow